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:
YohYamasaki
2026-07-04 23:08:21 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent f82b0a8fca
commit 9f9899cfd0
29 changed files with 1351 additions and 659 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,6 +37,7 @@ use graph_craft::application_io::wgpu_available;
use graph_craft::descriptor; use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::graphic::is_paint_present;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect; use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
@@ -44,7 +45,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::dvec2_to_point; 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 kurbo::{Affine, BezPath, Line, PathSeg};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
@@ -119,6 +120,12 @@ pub struct DocumentMessageHandler {
pub graph_view_overlay_open: bool, pub graph_view_overlay_open: bool,
/// The current opacity of the faded node graph background that covers up the artwork. /// The current opacity of the faded node graph background that covers up the artwork.
pub graph_fade_artwork_percentage: f64, 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 // Fields omitted from the saved document format
@@ -130,11 +137,6 @@ pub struct DocumentMessageHandler {
/// The path of the to the document file. /// The path of the to the document file.
#[serde(skip)] #[serde(skip)]
pub(crate) path: Option<PathBuf>, 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 /// 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)] #[serde(skip)]
breadcrumb_network_path: Vec<NodeId>, breadcrumb_network_path: Vec<NodeId>,
@@ -185,13 +187,13 @@ impl Default for DocumentMessageHandler {
graph_view_overlay_open: false, graph_view_overlay_open: false,
snapping_state: SnappingState::default(), snapping_state: SnappingState::default(),
graph_fade_artwork_percentage: 80., 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 // Fields omitted from the saved document format
// ============================================= // =============================================
name: DEFAULT_DOCUMENT_NAME.to_string(), name: DEFAULT_DOCUMENT_NAME.to_string(),
path: None, path: None,
// TODO: Eventually remove this document upgrade code
pending_gradient_migration: false,
breadcrumb_network_path: Vec::new(), breadcrumb_network_path: Vec::new(),
selection_network_path: Vec::new(), selection_network_path: Vec::new(),
history: DocumentHistory::default(), history: DocumentHistory::default(),
@@ -2770,8 +2772,9 @@ impl DocumentMessageHandler {
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer); 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); 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 { let has_fill = if let Some(list) = fill_graphic_list {
list.element(0).is_some() is_paint_present(list)
} else { } else {
!matches!(style.fill, Fill::None) !matches!(style.fill, Fill::None)
}; };
@@ -4011,6 +4014,21 @@ mod document_message_handler_tests {
use super::*; use super::*;
use crate::test_utils::test_prelude::*; 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] #[tokio::test]
async fn test_layer_selection_with_shift_and_ctrl() { async fn test_layer_selection_with_shift_and_ctrl() {
let mut editor = EditorTestUtils::create(); 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::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*; 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 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::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
@@ -15,7 +15,7 @@ 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::text::{Font, TypesettingConfig}; 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::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
@@ -454,29 +454,73 @@ impl<'a> ModifyInputsContext<'a> {
} }
pub fn fill_set(&mut self, fill: Fill) { 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 { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return; return;
}; };
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
match &fill { match &fill {
Fill::None => { Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), true); 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) => { Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true); 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) => { Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::FillGradient(gradient.clone()), false), true); 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) { 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. /// Write the gradient stops to the 'Gradient Value' node feeding the layer.
pub fn gradient_stops_set(&mut self, stops: GradientStops) { pub fn gradient_stops_set(&mut self, stops: GradientStops) {
let Some(output_layer) = self.get_output_layer() else { return }; 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); 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`). /// from the default (`Linear`).
pub fn gradient_type_set(&mut self, gradient_type: GradientType) { pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
let Some(output_layer) = self.get_output_layer() else { return }; let Some(output_layer) = self.get_output_layer() else { return };
let target_input = gradient_chain_target_input(output_layer, self.network_interface); let target_input = gradient_chain_target_input(output_layer, self.network_interface);
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER; let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
let create_if_nonexistent = gradient_type != GradientType::default(); let create_if_nonexistent = gradient_type != GradientType::default();
@@ -630,6 +709,7 @@ impl<'a> ModifyInputsContext<'a> {
/// from the default (`Pad`). /// from the default (`Pad`).
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) { pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
let Some(output_layer) = self.get_output_layer() else { return }; let Some(output_layer) = self.get_output_layer() else { return };
let target_input = gradient_chain_target_input(output_layer, self.network_interface); let target_input = gradient_chain_target_input(output_layer, self.network_interface);
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER; let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
let create_if_nonexistent = spread_method != GradientSpreadMethod::default(); let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
@@ -655,7 +735,7 @@ impl<'a> ModifyInputsContext<'a> {
return; 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); 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); 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); 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 } => { NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
use graphene_std::vector::generator_nodes::*; use graphene_std::vector::generator_nodes::*;
let is_fill = matches!(value, TaggedValue::Fill(_));
let reference = network_interface.reference(&node_id, selection_network_path); 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_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_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_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| { 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] [regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
.into_iter() .into_iter()
@@ -1782,7 +1782,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
input, input,
}); });
responses.add(PropertiesPanelMessage::Refresh); responses.add(PropertiesPanelMessage::Refresh);
if is_fill { if is_fill_input {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} }
if is_stroke_node || is_fill_node || is_shape_generator_node || is_text_node { 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::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::Graphic;
use graphene_std::NodeInputDecleration; use graphene_std::NodeInputDecleration;
use graphene_std::animation::RealTimeMode; use graphene_std::animation::RealTimeMode;
use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::color::SRGBA8;
use graphene_std::extract_xy::XY; use graphene_std::extract_xy::XY;
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::raster::{ 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::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType}; 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}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> { 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)) 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 /// 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. /// 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> { fn gradient_orientation_in_fill_node(node_id: NodeId, start: DVec2, end: DVec2, context: &mut NodePropertiesContext) -> Option<bool> {
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?; let layer = root_layer_for_chain_node(node_id, context)?;
let layer = LayerNodeIdentifier::new(layer_node, context.network_interface);
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface); let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
Some(graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform)) Some(graph_modification_utils::gradient_orientation_rightward(start, end, transform))
} }
/// Fill Node Widgets LayoutGroup /// Fill Node Widgets LayoutGroup
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::fill::*; use graphene_std::vector::fill::*;
#[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) // 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) { 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())) {
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 {
return vec![LayoutGroup::row(widgets_first_row)]; 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_color, backup_gradient) = match get_document_node(node_id, context) {
let backup_gradient_fill: Fill = backup_gradient.clone().into(); 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) let reverse_button = 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.")
.on_update(update_value( .on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput::<List<Graphic>>::INDEX))
{
let gradient = gradient.clone();
move |_| {
let mut gradient = gradient.clone();
gradient.stops = gradient.stops.reversed();
TaggedValue::Fill(Fill::Gradient(gradient))
}
},
node_id,
FillInput::<Color>::INDEX,
))
.widget_instance(); .widget_instance();
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push(reverse_button); 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), _ => 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(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push( widgets_first_row.push(
ColorInput::default() ColorInput::default()
.value(FillChoiceUI::from(&FillChoice::from(fill.clone()))) .value(fill_choice_ui)
.on_update(move |x: &ColorInput| { .on_update(move |x: &ColorInput| match &x.value {
let new_fill = FillChoice::from(&x.value).to_fill(fill2.as_gradient()); FillChoiceUI::None => solid_set_messages(None),
let (backup_index, backup_value) = match &new_fill { FillChoiceUI::Solid(srgba8) => {
Fill::None => (BackupColorInput::INDEX, TaggedValue::Color(None)), let color = Some(Color::from(*srgba8));
Fill::Solid(color) => (BackupColorInput::INDEX, TaggedValue::Color(Some(*color))), solid_set_messages(color)
Fill::Gradient(gradient) => (BackupGradientInput::INDEX, TaggedValue::FillGradient(gradient.clone())), }
}; FillChoiceUI::Gradient(gradient_stops_ui) => {
Message::Batched { let gradient = GradientStops::from(gradient_stops_ui);
messages: Box::new([ gradient_set_messages(gradient)
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(),
]),
} }
}) })
.on_commit(commit_value) .on_commit(commit_value)
@@ -2514,61 +2602,50 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let entries = vec![ let entries = vec![
RadioEntryData::new("solid") RadioEntryData::new("solid")
.label("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), .on_commit(commit_value),
RadioEntryData::new("gradient") RadioEntryData::new("gradient")
.label("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), .on_commit(commit_value),
]; ];
row.extend_from_slice(&[ row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), 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) LayoutGroup::row(row)
}; };
widgets.push(fill_type_switch); 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) // Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
let mut row = vec![TextLabel::new("").widget_instance()]; let mut row = vec![TextLabel::new("").widget_instance()];
add_blank_assist(&mut row); add_blank_assist(&mut row);
let gradient_for_closure = gradient.clone();
let entries = [GradientType::Linear, GradientType::Radial] let entries = [GradientType::Linear, GradientType::Radial]
.iter() .iter()
.map(|&grad_type| { .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)) RadioEntryData::new(format!("{:?}", grad_type))
.label(format!("{:?}", grad_type)) .label(format!("{:?}", grad_type))
.on_update(move |_| Message::Batched { .on_update(update_value(move |_| TaggedValue::GradientType(grad_type), node_id, GradientTypeInput::INDEX))
messages: Box::new([
set_input_value(&()),
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Type(grad_type),
}
.into(),
]),
})
.on_commit(commit_value) .on_commit(commit_value)
}) })
.collect(); .collect();
row.extend_from_slice(&[ row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), 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)); 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. // 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 mut spread_methods_row = vec![TextLabel::new("").widget_instance()];
let orientation_rightward = gradient_orientation_in_fill_node(node_id, &gradient, context).unwrap_or(true); // The button writes a value into the transform input, so only offer it when the input isn't wired
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) if transform_is_value {
.tooltip_label("Reverse Direction") let start = transform.transform_point2(DVec2::ZERO);
.tooltip_description(if gradient.gradient_type == GradientType::Radial { let end = transform.transform_point2(DVec2::X);
"Reverse which end the gradient radiates from." let new_transform = build_transform_with_y_preservation(transform, end, start);
} else { let orientation_rightward = gradient_orientation_in_fill_node(node_id, start, end, context).unwrap_or(true);
"Swap the start and end points of the gradient line."
}) let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
.on_update(update_value( .tooltip_label("Reverse Direction")
{ .tooltip_description(if gradient_type == GradientType::Radial {
let gradient = gradient.clone(); "Reverse which end the gradient radiates from."
move |_| { } else {
let mut gradient = gradient.clone(); "Swap the start and end points of the gradient line."
std::mem::swap(&mut gradient.start, &mut gradient.end); })
TaggedValue::Fill(Fill::Gradient(gradient)) .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());
node_id, spread_methods_row.push(reverse_direction_button);
FillInput::<Color>::INDEX, } else {
)) add_blank_assist(&mut spread_methods_row);
.widget_instance(); }
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
spread_methods_row.push(reverse_direction_button);
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat] let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
.iter() .iter()
.map(|&spread_method| { .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)) RadioEntryData::new(format!("{:?}", spread_method))
.label(format!("{:?}", spread_method)) .label(format!("{:?}", spread_method))
.on_update(move |_| Message::Batched { .on_update(update_value(move |_| TaggedValue::GradientSpreadMethod(spread_method), node_id, SpreadMethodInput::INDEX))
messages: Box::new([
set_input_value(&()),
set_backup_value(&()),
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(spread_method),
}
.into(),
]),
})
.on_commit(commit_value) .on_commit(commit_value)
}) })
.collect(); .collect();
spread_methods_row.extend_from_slice(&[ spread_methods_row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), 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)); 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) { let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node, Ok(document_node) => document_node,
Err(err) => { 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(); 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 miter_limit_disabled = join_value != &StrokeJoin::Miter;
let color = color_widget( 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(), 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.)); 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::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler; use crate::messages::prelude::DocumentMessageHandler;
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DVec2, IVec2}; use glam::{DVec2, IVec2};
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
use graph_craft::descriptor; use graph_craft::descriptor;
@@ -15,7 +14,7 @@ use graphene_std::ProtoNodeIdentifier;
use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType; use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId; 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::collections::HashMap;
use std::f64::consts::PI; use std::f64::consts::PI;
use std::ops::Range; 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) { pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
document.network_interface.migrate_path_modify_node(); 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(); let network = document.network_interface.document_network().clone();
// Apply string and node replacements to each node // 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; 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) // 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 { if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); 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(); let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
// TODO: Eventually remove this document upgrade code // 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 // A freshly-opened document with legacy gradients (whether newly decomposed or persisted from a save made before every bake landed) runs a
if document.pending_gradient_migration { // one-time measurement pre-pass instead of rendering, until every gradient's transform is baked into absolute space
self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses); if !document.pending_gradient_bbox_bake.is_empty() && self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses) {
return; return;
} }
@@ -5,9 +5,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
use crate::messages::prelude::*; use crate::messages::prelude::*;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::{ProtoNodeIdentifier, concrete}; use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::Color;
use graphene_std::NodeInputDecleration; use graphene_std::NodeInputDecleration;
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
@@ -15,8 +14,9 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster};
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
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, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType}; use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType};
use graphene_std::{Color, Graphic};
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.
@@ -271,11 +271,18 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center) network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center)
} }
/// Get the closest Fill node's ID to the provided layer, if any.
pub fn get_fill_node_id_with_direct_fill_input(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))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
matches!(fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?, NodeInput::Value { .. }).then_some(fill_node_id)
}
/// Determine the input connector where the gradient chain enters the layer. /// Determine the input connector where the gradient chain enters the layer.
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input. /// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector { pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) { if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX) InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)
} else { } else {
InputConnector::node(layer.to_node(), 1) InputConnector::node(layer.to_node(), 1)
} }
@@ -292,89 +299,29 @@ 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))) .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. /// 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> { 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))?; let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?; let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)? else { let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)? else {
return None; return None;
}; };
Some(*node_id) Some(*node_id)
} }
/// Get the current gradient of a layer from the closest "Fill" node.
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let TaggedValue::Fill(Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(gradient.clone())
}
// TODO: Eventually remove this document upgrade code
/// The legacy bounding-box-relative gradient (`absolute == false`) in a "Fill" node's active `fill` input, if any.
fn legacy_active_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
let TaggedValue::Fill(Fill::Gradient(gradient)) = node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)?.as_value()? else {
return None;
};
(!gradient.absolute).then(|| gradient.clone())
}
// TODO: Eventually remove this document upgrade code
/// The legacy bounding-box-relative gradient (`absolute == false`) stashed in a "Fill" node's `_backup_gradient` input, if any.
/// The backup is inert until the fill is toggled back to a gradient, at which point it becomes the active fill, so it needs converting too.
fn legacy_backup_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
let TaggedValue::FillGradient(gradient) = node.inputs.get(graphene_std::vector::fill::BackupGradientInput::INDEX)?.as_value()? else {
return None;
};
(!gradient.absolute).then(|| gradient.clone())
}
// TODO: Eventually remove this document upgrade code
/// Convert a "Fill" node's legacy gradients (the active `fill` and/or the stashed `_backup_gradient`) to absolute space using
/// the geometry's measured bounding box, writing each back in place. The active fill is written as a `Fill`, the backup as a bare `FillGradient`.
pub fn migrate_fill_node_gradients_to_absolute(fill_node_id: NodeId, network_interface: &mut NodeNetworkInterface, bounding_box: DAffine2, layer_transform: DAffine2) {
if let Some(gradient) = legacy_active_gradient_in_fill_node(fill_node_id, network_interface) {
let absolute = gradient.to_absolute(bounding_box, layer_transform);
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX);
network_interface.set_input(&input, NodeInput::value(TaggedValue::Fill(Fill::Gradient(absolute)), false), &[]);
}
if let Some(gradient) = legacy_backup_gradient_in_fill_node(fill_node_id, network_interface) {
let absolute = gradient.to_absolute(bounding_box, layer_transform);
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
network_interface.set_input(&input, NodeInput::value(TaggedValue::FillGradient(absolute), false), &[]);
}
}
// TODO: Eventually remove this document upgrade code
/// Find every root-network "Fill" node holding a legacy bounding-box-relative gradient, either as its active `fill` or as its `_backup_gradient`.
///
/// Scans the document network structurally instead of walking each layer's primary flow, so it also catches fills on
/// secondary inputs and in hidden, disabled, or orphaned branches. Fills nested inside subgraph node networks are skipped.
pub fn legacy_gradient_fill_nodes(network_interface: &NodeNetworkInterface) -> Vec<NodeId> {
let fill_identifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
network_interface
.document_network()
.nodes
.keys()
.copied()
.filter(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&fill_identifier))
.filter(|&node_id| legacy_active_gradient_in_fill_node(node_id, network_interface).is_some() || legacy_backup_gradient_in_fill_node(node_id, network_interface).is_some())
.collect()
}
/// Get the gradient stops of a layer, if any. /// Get the gradient stops of a layer, if any.
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> { pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> {
// Try to find the gradient stops value that is created by a Fill node first
if let Some(fill_node_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
return network_interface
.document_network()
.nodes
.get(&fill_node_id)
.and_then(|node| node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX))
.and_then(|input| input.as_value())
.and_then(|value| if let TaggedValue::Gradient(gradient) = value { Some(gradient.clone()) } else { None });
}
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
return None; return None;
@@ -398,14 +345,6 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
.unwrap_or(metadata.document_to_viewport); .unwrap_or(metadata.document_to_viewport);
} }
// 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) metadata.transform_to_viewport(layer)
} }
@@ -423,13 +362,11 @@ pub fn gradient_orientation_rightward(start: glam::DVec2, end: glam::DVec2, tran
/// 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 inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?; let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let &TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else { let &TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? else {
return None; return None;
}; };
Some(color) color
} }
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node. /// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
@@ -686,16 +623,74 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
} }
} }
/// A Fill node's decoded gradient inputs, with the transform kept in its raw form (not yet baked into `start`/`end`).
pub struct FillNodeGradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub spread_method: GradientSpreadMethod,
pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
pub transform_is_value: bool,
}
/// Decode a Fill node's gradient metadata inputs, resolving an unset transform to the default over `bounding_box`. Returns `None` when the fill input isn't a gradient value.
pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option<FillNodeGradient> {
use graphene_std::vector::fill;
let TaggedValue::Gradient(stops) = fill_node.inputs.get(fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? else {
return None;
};
let gradient_type = match fill_node.inputs.get(fill::GradientTypeInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientType(value)) => value,
_ => GradientType::default(),
};
let spread_method = match fill_node.inputs.get(fill::SpreadMethodInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
_ => GradientSpreadMethod::default(),
};
let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value());
let transform = match transform_input {
Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())),
_ => DAffine2::IDENTITY,
};
Some(FillNodeGradient {
stops: stops.clone(),
gradient_type,
spread_method,
transform,
transform_is_value: transform_input.is_some(),
})
}
// TODO: Update this to return Graphic once the legacy `Fill` enum has been eliminated
/// Returns the `Fill` value from a layer's upstream Fill node. /// Returns the `Fill` value from a layer's upstream Fill node.
pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> { pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> {
let fill_index = graphene_std::vector::fill::FillInput::<Fill>::INDEX; let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER), fill_index)?; let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
if let TaggedValue::Fill(fill) = tagged { Some(fill.clone()) } else { None }
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
&TaggedValue::Color(color) => Some(color.map_or(Fill::None, Fill::Solid)),
TaggedValue::Gradient(_) => {
let gradient = read_fill_node_gradient(fill_node, || network_interface.document_metadata().nonzero_bounding_box(layer))?;
Some(Fill::Gradient(Gradient {
stops: gradient.stops,
gradient_type: gradient.gradient_type,
spread_method: gradient.spread_method,
start: gradient.transform.transform_point2(DVec2::ZERO),
end: gradient.transform.transform_point2(DVec2::X),
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
}))
}
_ => None,
}
} }
/// Returns the stroke color from a layer's upstream Stroke node. /// Returns the stroke color from a layer's upstream Stroke node.
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> { pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
let color_index = graphene_std::vector::stroke::ColorInput::INDEX; let color_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?; let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?;
if let TaggedValue::Color(color) = tagged { Some(*color) } else { None } if let TaggedValue::Color(color) = tagged { Some(*color) } else { None }
} }
@@ -816,7 +811,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
for layer in layers { for layer in layers {
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) { if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
let input_index = graphene_std::vector::stroke::ColorInput::INDEX; let input_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
let value = TaggedValue::Color(color); let value = TaggedValue::Color(color);
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
} else { } else {
@@ -203,16 +203,16 @@ impl Fsm for FillToolFsmState {
mod test_fill { mod test_fill {
pub use crate::test_utils::test_prelude::*; pub use crate::test_utils::test_prelude::*;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::list::List;
use graphene_std::vector::fill; use graphene_std::vector::fill;
use graphene_std::vector::style::Fill;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> { async fn get_fills(editor: &mut EditorTestUtils) -> Vec<List<Color>> {
let instrumented = match editor.eval_graph().await { let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented, Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"), Err(e) => panic!("Failed to evaluate graph: {e}"),
}; };
instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect() instrumented.grab_all_input::<fill::FillInput<List<Color>>>(&editor.runtime).collect()
} }
#[tokio::test] #[tokio::test]
@@ -242,7 +242,8 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
let fills = get_fills(&mut editor).await; let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1); assert_eq!(fills.len(), 1);
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::GREEN)); let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN));
} }
#[tokio::test] #[tokio::test]
@@ -254,6 +255,7 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
let fills = get_fills(&mut editor).await; let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1); assert_eq!(fills.len(), 1);
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::YELLOW)); let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW));
} }
} }
@@ -8,7 +8,9 @@ use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasi
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::{FlowType, NodeNetworkInterface}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops, gradient_chain_target_input}; use crate::messages::tool::common_functionality::graph_modification_utils::{
self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input,
};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
@@ -355,7 +357,12 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
// TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot // TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> { fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
if let Some(stops) = get_gradient_stops(layer, network_interface) { if let Some(stops) = get_gradient_stops(layer, network_interface) {
// Try to construct a gradient out of a chain, which is directly connected to a layer // A Fill node holding a direct gradient value decodes through the shared reader
if get_fill_node_id_with_direct_fill_input(layer, network_interface).is_some() {
return graph_modification_utils::get_fill_value(layer, network_interface)?.as_gradient().cloned();
}
// Then, try to construct a gradient out of a chain, which is directly connected to a Fill node or a layer
let chain_state = read_gradient_chain_state(layer, network_interface); let chain_state = read_gradient_chain_state(layer, network_interface);
Some(Gradient { Some(Gradient {
stops, stops,
@@ -368,8 +375,7 @@ fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
}) })
} else { } else {
// Try to find a legacy Fill::Gradient that is selected in a Fill node None
graph_modification_utils::get_gradient(layer, network_interface)
} }
} }
@@ -481,7 +487,7 @@ struct SelectedGradient {
dragging: GradientDragTarget, dragging: GradientDragTarget,
initial_gradient: Gradient, initial_gradient: Gradient,
// TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `List<GradientStops>` replaces legacy `Fill::Gradient` // TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `List<GradientStops>` replaces legacy `Fill::Gradient`
is_gradient_list: bool, is_gradient_chain: 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> {
@@ -531,7 +537,7 @@ impl SelectedGradient {
gradient: gradient.clone(), gradient: gradient.clone(),
dragging: GradientDragTarget::End, dragging: GradientDragTarget::End,
initial_gradient: gradient, initial_gradient: gradient,
is_gradient_list: get_gradient_stops(layer, &document.network_interface).is_some(), is_gradient_chain: get_upstream_gradient_value_node_id(layer, &document.network_interface).is_some(),
} }
} }
@@ -748,7 +754,7 @@ impl SelectedGradient {
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 {
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>` // TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
if self.is_gradient_list { if self.is_gradient_chain {
dispatch_gradient_writes(layer, &self.gradient, responses); dispatch_gradient_writes(layer, &self.gradient, responses);
} else { } else {
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
@@ -1173,7 +1179,7 @@ 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 `List<GradientStops>` // TODO: Drop the legacy `Fill::Solid` branch when all gradients become `List<GradientStops>`
if selected_gradient.gradient.stops.len() == 1 { if selected_gradient.gradient.stops.len() == 1 {
if selected_gradient.is_gradient_list { if selected_gradient.is_gradient_chain {
selected_gradient.render_gradient(responses); selected_gradient.render_gradient(responses);
} else if let Some(layer) = selected_gradient.layer { } else if let Some(layer) = selected_gradient.layer {
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
@@ -1270,7 +1276,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_list = get_gradient_stops(layer, &document.network_interface).is_some(); let is_gradient_chain = get_upstream_gradient_value_node_id(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() {
@@ -1298,7 +1304,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_list, is_gradient_chain,
}); });
break; break;
@@ -1339,7 +1345,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_list, is_gradient_chain,
}); });
} }
} }
@@ -1356,7 +1362,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_list, is_gradient_chain,
}) })
} }
} }
@@ -1779,7 +1785,7 @@ fn apply_gradient_update(
// Only check for the gradient list once we know we'll write back, since this is a graph traversal per layer // Only check for the gradient list once we know we'll write back, since this is a graph traversal per layer
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>` // TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
if get_gradient_stops(layer, &context.document.network_interface).is_some() { if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
dispatch_gradient_writes(layer, &gradient, responses); dispatch_gradient_writes(layer, &gradient, responses);
} else { } else {
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
@@ -1821,7 +1827,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
continue; continue;
} }
if get_gradient_stops(layer, &context.document.network_interface).is_some() { if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() }); responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
updated_any_layer = true; updated_any_layer = true;
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) { } else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
@@ -1924,40 +1930,62 @@ mod test_gradient {
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}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
use crate::messages::tool::common_functionality::graph_modification_utils::get_fill_node_id_with_direct_fill_input;
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id;
pub use crate::test_utils::test_prelude::*; pub use crate::test_utils::test_prelude::*;
use glam::DAffine2; use glam::DAffine2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::vector::style::{Fill, Gradient}; use graphene_std::list::List;
use graphene_std::vector::style::{Gradient, GradientSpreadMethod};
use graphene_std::vector::{GradientStop, GradientStops, fill}; use graphene_std::vector::{GradientStop, GradientStops, fill};
use graphene_std::{Graphic, NodeInputDecleration};
use super::gradient_space_transform; use super::gradient_space_transform;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<(Fill, DAffine2)> { async fn get_gradients_from_fill(editor: &mut EditorTestUtils) -> Vec<(Gradient, DAffine2)> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
let document = editor.active_document(); let document = editor.active_document();
let layers = document.metadata().all_layers(); document
layers .metadata()
.all_layers()
.filter_map(|layer| { .filter_map(|layer| {
let fill = instrumented.grab_input_from_layer::<fill::FillInput<Fill>>(layer, &document.network_interface, &editor.runtime)?; // Only read Fill-owned gradient values, not chains
get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?;
let gradient = super::get_gradient(layer, &document.network_interface)?;
let transform = gradient_space_transform(layer, document); let transform = gradient_space_transform(layer, document);
Some((fill, transform)) Some((gradient, transform))
}) })
.collect() .collect()
} }
async fn get_gradient(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) { async fn get_gradients_from_chain(editor: &mut EditorTestUtils) -> Vec<(Gradient, DAffine2)> {
let fills = get_fills(editor).await; let document = editor.active_document();
assert_eq!(fills.len(), 1, "Expected 1 gradient fill, found {}", fills.len()); document
.metadata()
.all_layers()
.filter_map(|layer| {
// Only read actual gradient chains, not Fill-owned gradient values
get_upstream_gradient_value_node_id(layer, &document.network_interface)?;
let (fill, transform) = fills.first().unwrap(); let gradient = super::get_gradient(layer, &document.network_interface)?;
let gradient = fill.as_gradient().expect("Expected gradient fill type"); let transform = gradient_space_transform(layer, document);
Some((gradient, transform))
})
.collect()
}
(gradient.clone(), *transform) async fn get_gradient_from_fill(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) {
let gradients = get_gradients_from_fill(editor).await;
assert_eq!(gradients.len(), 1, "Expected 1 gradient fill, found {}", gradients.len());
gradients.into_iter().next().unwrap()
}
async fn get_gradient_from_chain(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) {
let gradients = get_gradients_from_chain(editor).await;
assert_eq!(gradients.len(), 1, "Expected 1 gradient chain, found {}", gradients.len());
gradients.into_iter().next().unwrap()
} }
fn assert_stops_at_positions(actual_positions: &[f64], expected_positions: &[f64], tolerance: f64) { fn assert_stops_at_positions(actual_positions: &[f64], expected_positions: &[f64], tolerance: f64) {
@@ -2010,13 +2038,51 @@ mod test_gradient {
layer layer
} }
async fn create_fill_gradient_chain_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 fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface).expect("Fill node should exist");
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(fill_node_id, fill::FillInput::<List<Graphic>>::INDEX),
})
.await;
editor
.handle_message(NodeGraphMessage::SetInputValue {
node_id: gradient_node_id,
input_index: 1,
value: TaggedValue::Gradient(GradientStops::new([
GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::RED,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::BLUE,
},
])),
})
.await;
layer
}
#[tokio::test] #[tokio::test]
async fn ignore_artboard() { async fn ignore_artboard() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
assert!(get_fills(&mut editor).await.is_empty()); assert!(get_gradients_from_fill(&mut editor).await.is_empty());
assert!(get_gradients_from_chain(&mut editor).await.is_empty());
} }
#[tokio::test] #[tokio::test]
@@ -2025,7 +2091,8 @@ mod test_gradient {
editor.new_document().await; editor.new_document().await;
editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await; editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await;
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
assert!(get_fills(&mut editor).await.is_empty()); assert!(get_gradients_from_fill(&mut editor).await.is_empty());
assert!(get_gradients_from_chain(&mut editor).await.is_empty());
} }
#[tokio::test] #[tokio::test]
@@ -2037,7 +2104,7 @@ mod test_gradient {
editor.select_secondary_color(Color::BLUE).await; editor.select_secondary_color(Color::BLUE).await;
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
let (gradient, transform) = get_gradient(&mut editor).await; let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
// Gradient goes from primary color to secondary color // Gradient goes from primary color to secondary color
let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>(); let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>();
@@ -2046,6 +2113,21 @@ mod test_gradient {
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10)); assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
} }
#[tokio::test]
async fn draw_updates_fill_gradient_chain_line() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = create_fill_gradient_chain_layer(&mut editor).await;
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
let (gradient, transform) = get_gradient_from_chain(&mut editor).await;
// Gradient line is updated while existing stops are preserved
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
}
#[tokio::test] #[tokio::test]
async fn snap_simple_draw() { async fn snap_simple_draw() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
@@ -2060,7 +2142,7 @@ mod test_gradient {
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Gradient, start.x, start.y, end.x, end.y, ModifierKeys::SHIFT).await; editor.drag_tool(ToolType::Gradient, start.x, start.y, end.x, end.y, ModifierKeys::SHIFT).await;
let (gradient, transform) = get_gradient(&mut editor).await; let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
assert!(transform.transform_point2(gradient.start).abs_diff_eq(start, 1e-10)); assert!(transform.transform_point2(gradient.start).abs_diff_eq(start, 1e-10));
@@ -2103,7 +2185,7 @@ mod test_gradient {
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
let (gradient, transform) = get_gradient(&mut editor).await; let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10)); assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10)); assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
@@ -2120,7 +2202,7 @@ mod test_gradient {
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
// Get initial gradient state (should have 2 stops) // Get initial gradient state (should have 2 stops)
let (initial_gradient, _) = get_gradient(&mut editor).await; let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len()); assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
editor.select_tool(ToolType::Gradient).await; editor.select_tool(ToolType::Gradient).await;
@@ -2129,7 +2211,7 @@ mod test_gradient {
editor.left_mouseup(25., 0., ModifierKeys::empty()).await; editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
// Check that a new stop has been added // Check that a new stop has been added
let (updated_gradient, _) = get_gradient(&mut editor).await; let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len()); assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len());
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect(); let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
@@ -2165,7 +2247,7 @@ mod test_gradient {
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
// Get the initial gradient state // Get the initial gradient state
let (initial_gradient, transform) = get_gradient(&mut editor).await; let (initial_gradient, transform) = get_gradient_from_fill(&mut editor).await;
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len()); assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
// Verify initial gradient endpoints in viewport space // Verify initial gradient endpoints in viewport space
@@ -2195,7 +2277,7 @@ mod test_gradient {
.await; .await;
// Check the updated gradient // Check the updated gradient
let (updated_gradient, transform) = get_gradient(&mut editor).await; let (updated_gradient, transform) = get_gradient_from_fill(&mut editor).await;
// Verify the start point hasn't changed // Verify the start point hasn't changed
let updated_start = transform.transform_point2(updated_gradient.start); let updated_start = transform.transform_point2(updated_gradient.start);
@@ -2223,7 +2305,7 @@ mod test_gradient {
editor.left_mousedown(25., 0., ModifierKeys::empty()).await; editor.left_mousedown(25., 0., ModifierKeys::empty()).await;
editor.left_mouseup(25., 0., ModifierKeys::empty()).await; editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
let (initial_gradient, _) = get_gradient(&mut editor).await; let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len()); assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len());
// Verify initial stop positions and colors // Verify initial stop positions and colors
@@ -2262,7 +2344,7 @@ mod test_gradient {
) )
.await; .await;
let (updated_gradient, _) = get_gradient(&mut editor).await; let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops after dragging, found {}", updated_gradient.stops.len()); assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops after dragging, found {}", updated_gradient.stops.len());
// Verify updated stop positions and colors // Verify updated stop positions and colors
@@ -2290,7 +2372,7 @@ mod test_gradient {
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
// Get initial gradient state (should have 2 stops) // Get initial gradient state (should have 2 stops)
let (initial_gradient, _) = get_gradient(&mut editor).await; let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len()); assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
editor.select_tool(ToolType::Gradient).await; editor.select_tool(ToolType::Gradient).await;
@@ -2304,7 +2386,7 @@ mod test_gradient {
editor.left_mousedown(75., 0., ModifierKeys::empty()).await; editor.left_mousedown(75., 0., ModifierKeys::empty()).await;
editor.left_mouseup(75., 0., ModifierKeys::empty()).await; editor.left_mouseup(75., 0., ModifierKeys::empty()).await;
let (updated_gradient, _) = get_gradient(&mut editor).await; let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len()); assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len());
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect(); let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
@@ -2330,7 +2412,7 @@ mod test_gradient {
editor.press(Key::Delete, ModifierKeys::empty()).await; editor.press(Key::Delete, ModifierKeys::empty()).await;
// Verify we now have 3 stops // Verify we now have 3 stops
let (final_gradient, _) = get_gradient(&mut editor).await; let (final_gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len()); assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len());
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|stop| stop.position).collect(); let final_positions: Vec<f64> = final_gradient.stops.iter().map(|stop| stop.position).collect();
@@ -2374,15 +2456,13 @@ mod test_gradient {
#[tokio::test] #[tokio::test]
async fn change_spread_method() { async fn change_spread_method() {
use graphene_std::vector::style::GradientSpreadMethod;
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await;
// Verify default spread method is Pad // Verify default spread method is Pad
let (gradient, _) = get_gradient(&mut editor).await; let (gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad); assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
// Update spread method to Repeat // Update spread method to Repeat
@@ -2392,7 +2472,7 @@ mod test_gradient {
}) })
.await; .await;
let (gradient, _) = get_gradient(&mut editor).await; let (gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat); assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
// Update spread method to Reflect // Update spread method to Reflect
@@ -2402,12 +2482,45 @@ mod test_gradient {
}) })
.await; .await;
let (gradient, _) = get_gradient(&mut editor).await; let (gradient, _) = get_gradient_from_fill(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect); assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
} }
#[tokio::test] #[tokio::test]
async fn gradient_list_drag_endpoint() { async fn change_spread_method_chain() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let layer = create_fill_gradient_chain_layer(&mut editor).await;
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
editor.select_tool(ToolType::Gradient).await;
// Verify default spread method is Pad
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
// Update spread method to Repeat
editor
.handle_message(GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
})
.await;
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
// Update spread method to Reflect
editor
.handle_message(GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
})
.await;
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
}
#[tokio::test]
async fn gradient_list_layer_drag_endpoint() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
let layer = create_gradient_list_layer(&mut editor).await; let layer = create_gradient_list_layer(&mut editor).await;
@@ -2479,7 +2592,7 @@ mod test_gradient {
} }
#[tokio::test] #[tokio::test]
async fn gradient_list_preserves_stops() { async fn gradient_list_layer_preserves_stops() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
let layer = create_gradient_list_layer(&mut editor).await; let layer = create_gradient_list_layer(&mut editor).await;
+148 -48
View File
@@ -1,10 +1,10 @@
use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2, UVec2}; use glam::{DAffine2, DVec2, UVec2};
use graph_craft::application_io::EditorPreferences; use graph_craft::application_io::EditorPreferences;
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation}; use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation};
use graphene_std::bounds::RenderBoundingBox; use graphene_std::bounds::RenderBoundingBox;
@@ -15,7 +15,8 @@ use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::{ATTR_TRANSFORM, Context, Graphic}; use graphene_std::vector::style::Gradient;
use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -68,6 +69,10 @@ pub struct NodeGraphExecutor {
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// In-progress one-time pre-pass that converts legacy bounding-box-relative gradients to absolute space, if any. /// In-progress one-time pre-pass that converts legacy bounding-box-relative gradients to absolute space, if any.
gradient_migration: Option<GradientMigration>, gradient_migration: Option<GradientMigration>,
// TODO: Eventually remove this document upgrade code
/// Documents whose gradient migration pass already ran this session, so entries that failed to measure
/// (kept pending for a retry on the next open) don't re-run the pass on every render request.
gradient_migration_attempted: HashSet<DocumentId>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -75,18 +80,20 @@ struct ExecutionContext {
export_config: Option<ExportConfig>, export_config: Option<ExportConfig>,
document_id: DocumentId, document_id: DocumentId,
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Set when this execution is a gradient-migration measurement run for the given "Fill" node, whose evaluated geometry /// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
/// is read back from the inspect result to size the gradient; such runs never touch the visible artwork. /// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
measure_fill: Option<NodeId>, /// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured.
measure_fill: Option<(Vec<NodeId>, NodeId, Gradient)>,
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// State for the deferred legacy-gradient migration: a queue of root-network "Fill" nodes still holding bounding-box-relative /// State for the deferred legacy-gradient migration: a queue of "Fill" nodes (each addressed by its enclosing network path)
/// gradients, measured one at a time by redirecting the document export to each so even hidden/orphaned branches evaluate. /// whose decomposed gradient still needs its transform baked, each paired with its original relative gradient and measured
/// one at a time by redirecting the document export so even hidden/orphaned/nested branches evaluate.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct GradientMigration { struct GradientMigration {
document_id: DocumentId, document_id: DocumentId,
remaining: VecDeque<NodeId>, remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)>,
resolution: UVec2, resolution: UVec2,
scale: f64, scale: f64,
} }
@@ -106,6 +113,7 @@ impl NodeGraphExecutor {
current_execution_id: 0, current_execution_id: 0,
previous_node_to_inspect: Vec::new(), previous_node_to_inspect: Vec::new(),
gradient_migration: None, gradient_migration: None,
gradient_migration_attempted: HashSet::new(),
}; };
(node_runtime, node_executor) (node_runtime, node_executor)
} }
@@ -342,6 +350,12 @@ impl NodeGraphExecutor {
} }
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> { pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> {
// TODO: Eventually remove this document upgrade code
// A gradient migration belongs to one document; if the active document changed away from it, cancel so its stale in-flight response is ignored and revisiting restarts a fresh pass
if self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id != document_id) {
self.cancel_gradient_migration();
}
let results = self.runtime_io.receive().collect::<Vec<_>>(); let results = self.runtime_io.receive().collect::<Vec<_>>();
for response in results { for response in results {
match response { match response {
@@ -370,9 +384,9 @@ impl NodeGraphExecutor {
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
// Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork. // Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork.
// Apply only to the active document's in-progress migration, dropping responses left over from a document switch. // Apply only to the active document's in-progress migration, dropping responses left over from a document switch.
if let Some(fill_node_id) = execution_context.measure_fill { if let Some(bake_target) = execution_context.measure_fill {
if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) { if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) {
self.handle_gradient_measurement(document, document_id, fill_node_id, result.ok().and(inspect_result), responses); self.handle_gradient_measurement(document, document_id, bake_target, result.ok().and(inspect_result), responses);
} }
continue; continue;
} }
@@ -463,20 +477,25 @@ impl NodeGraphExecutor {
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Kick off the one-time pre-pass converting legacy bounding-box gradients to absolute space, or no-op if already running. /// Kick off the one-time pre-pass converting legacy bounding-box gradients to absolute space, or no-op if already running.
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) { /// Returns false when the render should proceed normally instead: the pass already ran this session, so any entries still
/// pending are unmeasurable ones kept to retry on the next open.
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) -> bool {
if self.gradient_migration_attempted.contains(&document_id) {
return false;
}
match &self.gradient_migration { match &self.gradient_migration {
// Already running for this document // Already running for this document
Some(migration) if migration.document_id == document_id => return, Some(migration) if migration.document_id == document_id => return true,
// A different document's migration is in progress; cancel it so this one can run (the other restarts when revisited) // A different document's migration is in progress; cancel it so this one can run (the other restarts when revisited)
Some(_) => self.gradient_migration = None, Some(_) => self.gradient_migration = None,
None => {} None => {}
} }
let remaining: VecDeque<NodeId> = graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).into_iter().collect(); // Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands.
let Some(&first_fill) = remaining.front() else { let remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
document.pending_gradient_migration = false; let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
responses.add(NodeGraphMessage::RunDocumentGraph); return false;
return;
}; };
self.gradient_migration = Some(GradientMigration { self.gradient_migration = Some(GradientMigration {
@@ -485,36 +504,39 @@ impl NodeGraphExecutor {
resolution, resolution,
scale, scale,
}); });
self.dispatch_gradient_measurement(document, document_id, first_fill, resolution, scale, responses); self.dispatch_gradient_measurement(document, document_id, first_network_path, first_fill, first_gradient, resolution, scale, responses);
true
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Run the graph with its export redirected to `fill_node_id` (so its branch evaluates even when hidden or orphaned) and /// Run the graph with its export chain redirected down to the (possibly nested) Fill at `network_path`/`fill_node_id`
/// that node inspected, so its evaluated geometry returns in the execution response without touching the visible artwork. /// (so its branch evaluates even when hidden or orphaned) and that node inspected, so its evaluated geometry returns
/// in the execution response without touching the visible artwork.
#[allow(clippy::too_many_arguments)]
fn dispatch_gradient_measurement( fn dispatch_gradient_measurement(
&mut self, &mut self,
document: &mut DocumentMessageHandler, document: &mut DocumentMessageHandler,
document_id: DocumentId, document_id: DocumentId,
network_path: Vec<NodeId>,
fill_node_id: NodeId, fill_node_id: NodeId,
gradient: Gradient,
resolution: UVec2, resolution: UVec2,
scale: f64, scale: f64,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) { ) {
let mut network = document.network_interface.document_network().clone(); let mut network = document.network_interface.document_network().clone();
// On this throwaway clone, un-hide just the Fill so it's measured as a real Fill rather than a passthrough. // On this throwaway clone, redirect each level's export down to the Fill, un-hiding the Fill and its enclosing subnetworks so it's measured as a real Fill rather than a passthrough.
// But upstream generators keep their visibility, so a hidden one intentionally contributes no geometry. // But upstream generators keep their visibility, so a hidden one intentionally contributes no geometry.
if let Some(node) = network.nodes.get_mut(&fill_node_id) { let full_path: Vec<NodeId> = network_path.iter().copied().chain(std::iter::once(fill_node_id)).collect();
node.visible = true; if !redirect_export_chain(&mut network, &full_path) {
} // The fill was deleted or is otherwise unreachable, so drop its stale entry instead of retrying it on every open
log::warn!("Gradient migration: could not redirect the document network's export chain to fill node {fill_node_id:?}; dropping its pending gradient bake");
let Some(export) = network.exports.first_mut() else { remove_pending_gradient_bake(document, &network_path, fill_node_id);
// No export to redirect through, so skip this Fill rather than leaving the migration stuck
log::warn!("Gradient migration: document network has no export to redirect");
self.advance_gradient_migration(document, document_id, responses); self.advance_gradient_migration(document, document_id, responses);
return; return;
}; }
*export = NodeInput::node(fill_node_id, 0);
let resources = document.resources.registry.clone(); let resources = document.resources.registry.clone();
if self if self
@@ -522,7 +544,7 @@ impl NodeGraphExecutor {
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
network, network,
resources, resources,
node_to_inspect: vec![fill_node_id], node_to_inspect: full_path.clone(),
})) }))
.is_err() .is_err()
{ {
@@ -533,7 +555,7 @@ impl NodeGraphExecutor {
// Force the next normal render to recompile and re-send the real, non-redirected network // Force the next normal render to recompile and re-send the real, non-redirected network
self.node_graph_hash = 0; self.node_graph_hash = 0;
self.previous_node_to_inspect = vec![fill_node_id]; self.previous_node_to_inspect = full_path;
let viewport = Footprint { let viewport = Footprint {
transform: document.metadata().document_to_viewport, transform: document.metadata().document_to_viewport,
@@ -556,26 +578,50 @@ impl NodeGraphExecutor {
ExecutionContext { ExecutionContext {
export_config: None, export_config: None,
document_id, document_id,
measure_fill: Some(fill_node_id), measure_fill: Some((network_path, fill_node_id, gradient)),
}, },
)); ));
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Convert the just-measured fill's legacy gradients to absolute space using its evaluated geometry, then advance the queue. /// Bake the just-measured fill's gradient transform into absolute space using its evaluated geometry, then advance the queue.
fn handle_gradient_measurement( fn handle_gradient_measurement(
&mut self, &mut self,
document: &mut DocumentMessageHandler, document: &mut DocumentMessageHandler,
document_id: DocumentId, document_id: DocumentId,
fill_node_id: NodeId, bake_target: (Vec<NodeId>, NodeId, Gradient),
inspect_result: Option<InspectResult>, inspect_result: Option<InspectResult>,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) { ) {
let measured = inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data)); let (network_path, fill_node_id, gradient) = bake_target;
match measured { // Ignore a stale response whose fill is no longer the one being measured (a re-dispatch after a document switch can leave two measurements in flight for the same entry)
Some((bounding_box, item_transform)) => graph_modification_utils::migrate_fill_node_gradients_to_absolute(fill_node_id, &mut document.network_interface, bounding_box, item_transform), let is_current = self
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving it in legacy space"), .gradient_migration
.as_ref()
.and_then(|migration| migration.remaining.front())
.is_some_and(|(front_path, front_fill, _)| *front_fill == fill_node_id && *front_path == network_path);
if !is_current {
return;
}
match inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data)) {
Some((bounding_box, item_transform)) => {
// Skip the bake if the user already placed this gradient themselves, but drop the now-superseded entry either way
if fill_transform_unbaked(document, &network_path, fill_node_id) {
let absolute_gradient = gradient.to_absolute(bounding_box, item_transform);
let gradient_transform = absolute_gradient.transform * absolute_gradient.to_transform();
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX);
document
.network_interface
.set_input(&input, NodeInput::value(TaggedValue::OptionalDAffine2(Some(gradient_transform)), false), &network_path);
}
// The transform is settled, so its entry no longer needs to persist for a retry on the next open
remove_pending_gradient_bake(document, &network_path, fill_node_id);
}
// Leave the entry pending so the next open retries it
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving its transform unbaked and pending a retry on the next open"),
} }
self.advance_gradient_migration(document, document_id, responses); self.advance_gradient_migration(document, document_id, responses);
@@ -584,23 +630,30 @@ impl NodeGraphExecutor {
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Move to the next queued fill, or finish the migration and trigger a normal render once the queue is empty. /// Move to the next queued fill, or finish the migration and trigger a normal render once the queue is empty.
fn advance_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) { fn advance_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
let next_fill = self.gradient_migration.as_mut().and_then(|migration| { let next = self.gradient_migration.as_mut().and_then(|migration| {
migration.remaining.pop_front(); migration.remaining.pop_front();
migration.remaining.front().copied() migration.remaining.front().cloned()
}); });
if let Some(next_fill) = next_fill { if let Some((next_network_path, next_fill, next_gradient)) = next {
let (resolution, scale) = self.gradient_migration.as_ref().map(|migration| (migration.resolution, migration.scale)).unwrap_or((UVec2::ONE, 1.)); let (resolution, scale) = self.gradient_migration.as_ref().map(|migration| (migration.resolution, migration.scale)).unwrap_or((UVec2::ONE, 1.));
self.dispatch_gradient_measurement(document, document_id, next_fill, resolution, scale, responses); self.dispatch_gradient_measurement(document, document_id, next_network_path, next_fill, next_gradient, resolution, scale, responses);
} else { } else {
self.gradient_migration = None; // Entries still pending failed to measure; they stay persisted for the next open while this marker stops re-runs this session
self.previous_node_to_inspect = Vec::new(); self.cancel_gradient_migration();
self.node_graph_hash = 0; self.gradient_migration_attempted.insert(document_id);
document.pending_gradient_migration = false;
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
} }
// TODO: Eventually remove this document upgrade code
/// Tear down the in-progress migration and clear the stale inspect target and hash so the next normal render recompiles the real network.
fn cancel_gradient_migration(&mut self) {
self.gradient_migration = None;
self.previous_node_to_inspect = Vec::new();
self.node_graph_hash = 0;
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> { fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(render_output) = node_graph_output else { let TaggedValue::RenderOutput(render_output) = node_graph_output else {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}")); return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
@@ -768,6 +821,53 @@ impl NodeGraphExecutor {
} }
} }
// TODO: Eventually remove this document upgrade code
/// Whether the fill node's transform input is still the unset `OptionalDAffine2(None)` placeholder that the migration leaves
/// behind, meaning its gradient placement has not yet been baked (or set by the user), so a measured bake may safely be written.
fn fill_transform_unbaked(document: &DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) -> bool {
let Some(network) = document.network_interface.document_network().nested_network(network_path) else {
return false;
};
let Some(node) = network.nodes.get(&fill_node_id) else { return false };
matches!(
node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX).and_then(|input| input.as_value()),
Some(TaggedValue::OptionalDAffine2(None))
)
}
// TODO: Eventually remove this document upgrade code
/// Remove a fill's entry from the document's persisted pending-bake list, either because its bake landed or because it is stale.
fn remove_pending_gradient_bake(document: &mut DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) {
if let Some(index) = document
.pending_gradient_bbox_bake
.iter()
.position(|(path, node_id, _)| *node_id == fill_node_id && path == network_path)
{
document.pending_gradient_bbox_bake.remove(index);
}
}
// TODO: Eventually remove this document upgrade code
/// Redirect the primary export at each level of `network` down through the subnetwork nodes named by `full_path`,
/// un-hiding each so the innermost node's branch evaluates. Returns false if any step is missing or not a subnetwork.
fn redirect_export_chain(network: &mut NodeNetwork, full_path: &[NodeId]) -> bool {
let Some((node_id, deeper_path)) = full_path.split_first() else { return true };
let Some(export) = network.exports.first_mut() else { return false };
*export = NodeInput::node(*node_id, 0);
let Some(node) = network.nodes.get_mut(node_id) else { return false };
node.visible = true;
if deeper_path.is_empty() {
return true;
}
match &mut node.implementation {
DocumentNodeImplementation::Network(nested) => redirect_export_chain(nested, deeper_path),
_ => false,
}
}
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Turn a measured fill node's evaluated geometry into a `[0,1]² -> bounding box` affine plus the geometry's item transform. /// Turn a measured fill node's evaluated geometry into a `[0,1]² -> bounding box` affine plus the geometry's item transform.
fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> { fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> {
@@ -402,6 +402,7 @@ tagged_value! {
DVec2(DVec2), DVec2(DVec2),
#[serde(alias = "Affine2")] #[serde(alias = "Affine2")]
DAffine2(DAffine2), DAffine2(DAffine2),
OptionalDAffine2(Option<DAffine2>),
FillGradient(Gradient), FillGradient(Gradient),
Font(Font), Font(Font),
Footprint(Footprint), Footprint(Footprint),
@@ -583,6 +584,8 @@ impl TaggedValue {
// `Color` (not in a `List`) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants // `Color` (not in a `List`) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants
() if ty == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, () if ty == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, () if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
() if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?, () if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?, () if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, () if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
@@ -127,6 +127,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
@@ -673,6 +673,21 @@ impl ItemAttributeValues {
} }
self.0.push((to_key, value)); self.0.push((to_key, value));
} }
/// Clones the attribute with `key` from `source`, replacing any existing attribute with the same key.
pub fn insert_cloned_from(&mut self, source: &Self, key: &str) {
let Some((_, value)) = source.0.iter().find(|(existing_key, _)| existing_key == key) else {
return;
};
let value = value.clone();
if let Some((_, existing_value)) = self.0.iter_mut().find(|(existing_key, _)| existing_key == key) {
*existing_value = value;
} else {
self.0.push((key.to_string(), value));
}
}
} }
// ========== // ==========
@@ -1,6 +1,6 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List}; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List};
use core_types::ops::{FromAnchorPosition, ListConvert}; use core_types::ops::{FromAnchorPosition, ListConvert};
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
@@ -215,29 +215,34 @@ pub fn color_to_graphic_list(color: Option<Color>) -> Option<List<Graphic>> {
color.as_ref().map(|color| List::new_from_element((*color).into())) color.as_ref().map(|color| List::new_from_element((*color).into()))
} }
/// Look up the paint graphics stored under attribute for a vector item, normalizing any graphic list type to `List<Graphic>`. /// Whether a normalized paint graphic list actually carries renderable paint.
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
}
/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List<Graphic>` form.
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> { pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
list.attribute::<List<Graphic>>(attribute, index) list.attribute::<List<Graphic>>(attribute, index)
.map(Cow::Borrowed) .map(Cow::Borrowed)
.or_else(|| list.attribute::<List<Color>>(attribute, index).map(|c| Cow::Owned(c.clone().into_graphic_list())))
.or_else(|| list.attribute::<List<GradientStops>>(attribute, index).map(|g| Cow::Owned(g.clone().into_graphic_list())))
.or_else(|| list.attribute::<List<Vector>>(attribute, index).map(|v| Cow::Owned(v.clone().into_graphic_list())))
.or_else(|| list.attribute::<List<Raster<CPU>>>(attribute, index).map(|r| Cow::Owned(r.clone().into_graphic_list())))
.or_else(|| list.attribute::<List<Raster<GPU>>>(attribute, index).map(|r| Cow::Owned(r.clone().into_graphic_list())))
// Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it. // Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it.
.filter(|graphic_list| graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())) .filter(|graphic_list| is_paint_present(graphic_list))
} }
/// Whether the item carries a non-blank paint attribute in any representation (`Graphic`, `Color`, /// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute,
/// `GradientStops`, `Vector`, or raster), checked by borrowing without cloning the renderable list. /// checked by borrowing without cloning the renderable list.
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool { pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
list.attribute::<List<Graphic>>(attribute, index) list.attribute::<List<Graphic>>(attribute, index).is_some_and(is_paint_present)
.is_some_and(|graphics| graphics.element(0).is_some_and(|graphic| !graphic.is_empty())) }
|| list.attribute::<List<Color>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|| list.attribute::<List<GradientStops>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty()) /// Stores a paint attribute in its canonical `List<Graphic>` form, the only representation paint readers accept.
|| list.attribute::<List<Vector>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty()) pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
|| list.attribute::<List<Raster<CPU>>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty()) attributes.insert(key, paint.into_graphic_list());
|| list.attribute::<List<Raster<GPU>>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty()) }
/// Stores a paint attribute at a list index in its canonical `List<Graphic>` form, the only representation paint readers accept.
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
list.set_attribute(key, index, paint.into_graphic_list());
} }
/// Look up the fill paint graphics for a vector item, falling back to the legacy /// Look up the fill paint graphics for a vector item, falling back to the legacy
@@ -320,6 +325,36 @@ pub fn is_stroke_fully_transparent_at(list: &List<Vector>, index: usize) -> bool
color.a() == 0. color.a() == 0.
} }
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// canonical `List<Graphic>` fill and stroke attributes.
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*item_transform = transform * *item_transform;
}
}
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
for graphic in graphics.iter_element_values_mut() {
match graphic {
Graphic::Graphic(list) => bake_list_transform(list, transform),
Graphic::Vector(list) => bake_list_transform(list, transform),
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
Graphic::Gradient(list) => bake_list_transform(list, transform),
Graphic::Text(list) => bake_list_transform(list, transform),
Graphic::Color(_) => {}
}
}
}
for paint_key in [ATTR_FILL, ATTR_STROKE] {
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
bake_graphic_paint_transform(graphics, transform);
}
}
}
/// Maps from a concrete element type to its corresponding `Graphic` enum variant, /// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `List`s from a `Graphic` value. /// enabling type-directed casting of typed `List`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized { pub trait TryFromGraphic: Clone + Sized {
@@ -357,7 +392,7 @@ impl TryFromGraphic for String {
} }
// Local trait to convert types to List<Graphic> (avoids orphan rule issues) // Local trait to convert types to List<Graphic> (avoids orphan rule issues)
pub trait IntoGraphicList { pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
fn into_graphic_list(self) -> List<Graphic>; fn into_graphic_list(self) -> List<Graphic>;
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`. /// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
@@ -21,7 +21,7 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper; use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource; use graphene_resource::Resource;
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, stroke_graphic_list_at}; use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, stroke_graphic_list_at};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
@@ -1128,11 +1128,12 @@ impl Render for List<Vector> {
let mut cloned_vector = vector.clone(); let mut cloned_vector = vector.clone();
cloned_vector.style.clear_stroke(); cloned_vector.style.clear_stroke();
cloned_vector.style.set_fill(Fill::solid(Color::BLACK));
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior. // The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
// The wrapping SVG group (above) handles the user-set opacity. // The wrapping SVG group (above) handles the user-set opacity.
let vector_item = List::new_from_item(Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform)); let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
let vector_item = List::new_from_item(mask_item);
(id, mask_type, vector_item) (id, mask_type, vector_item)
}); });
@@ -1197,7 +1198,7 @@ impl Render for List<Vector> {
.style .style
.stroke() .stroke()
.map(|stroke| { .map(|stroke| {
if stroke_graphic_list.as_ref().and_then(|l| l.element(0)).is_some() { if stroke_graphic_list.as_deref().is_some_and(is_paint_present) {
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke) stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
} else { } else {
String::new() String::new()
@@ -1474,11 +1475,12 @@ impl Render for List<Vector> {
if use_layer { if use_layer {
let mut cloned_element = element.clone(); let mut cloned_element = element.clone();
cloned_element.style.clear_stroke(); cloned_element.style.clear_stroke();
cloned_element.style.set_fill(Fill::solid(Color::BLACK));
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior. // The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
// The outer opacity/blend layer (above) handles the user-set opacity. // The outer opacity/blend layer (above) handles the user-set opacity.
let vector_list = List::new_from_item(Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform)); let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
let vector_list = List::new_from_item(mask_item);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed // This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
@@ -458,6 +458,15 @@ impl GradientStops {
result result
} }
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let stops = self.iter().zip(other.iter()).map(|(a, b)| {
let position = a.position + (b.position - a.position) * time;
let color = a.color.lerp(&b.color, time as f32);
GradientStop { position, midpoint: 0.5, color }
});
GradientStops::new(stops)
}
} }
#[repr(C)] #[repr(C)]
@@ -601,30 +610,6 @@ impl Gradient {
} }
} }
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let stops = self.stops.iter().zip(other.stops.iter()).map(|(a, b)| {
let position = a.position + (b.position - a.position) * time;
let color = a.color.lerp(&b.color, time as f32);
GradientStop { position, midpoint: 0.5, color }
});
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
let spread_method = if time < 0.5 { self.spread_method } else { other.spread_method };
Self {
start,
end,
stops,
gradient_type,
spread_method,
// TODO: Eventually remove this document upgrade code
absolute: self.absolute,
transform: if time < 0.5 { self.transform } else { other.transform },
}
}
/// Insert a stop into the gradient, the index if successful /// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> { pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse. // Transform the start and end positions to the same coordinate space as the mouse.
@@ -663,6 +648,54 @@ impl Gradient {
} }
} }
/// 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.
pub 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,
}
}
/// Build the default transform for a gradient not yet given one: a horizontal gradient spanning the
/// bounding box's width, running through its vertical middle.
pub fn initial_gradient_transform_for_bounding_box(bounds: [DVec2; 2]) -> DAffine2 {
let [min, max] = bounds;
let x_axis = DVec2::new(max.x - min.x, 0.);
DAffine2 {
matrix2: glam::DMat2::from_cols(x_axis, x_axis.perp()),
translation: DVec2::new(min.x, (min.y + max.y) / 2.),
}
}
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
pub fn migrate_to_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientStops, D::Error> { pub fn migrate_to_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientStops, D::Error> {
use serde::Deserialize; use serde::Deserialize;
@@ -66,30 +66,6 @@ impl Fill {
} }
} }
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let transparent = Self::solid(Color::TRANSPARENT);
let a = if *self == Self::None && *other != Self::None { &transparent } else { self };
let b = if *other == Self::None && *self != Self::None { &transparent } else { other };
match (a, b) {
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
(Self::Solid(a), Self::Gradient(b)) => {
let mut solid_to_gradient = b.clone();
solid_to_gradient.stops.color.iter_mut().for_each(|color| *color = *a);
let a = &solid_to_gradient;
Self::Gradient(a.lerp(b, time))
}
(Self::Gradient(a), Self::Solid(b)) => {
let mut gradient_to_solid = a.clone();
gradient_to_solid.stops.color.iter_mut().for_each(|color| *color = *b);
let b = &gradient_to_solid;
Self::Gradient(a.lerp(b, time))
}
(Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)),
(Self::None, _) | (_, Self::None) => Self::None,
}
}
/// Extract a gradient from the fill /// Extract a gradient from the fill
pub fn as_gradient(&self) -> Option<&Gradient> { pub fn as_gradient(&self) -> Option<&Gradient> {
match self { match self {
@@ -389,7 +365,8 @@ fn daffine2_identity() -> DAffine2 {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct Stroke { pub struct Stroke {
/// Stroke color /// Deprecated, use `ATTR_STROKE` instead.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub color: Option<Color>, pub color: Option<Color>,
/// Line thickness /// Line thickness
pub weight: f64, pub weight: f64,
@@ -632,30 +609,6 @@ impl PathStyle {
Self { stroke, fill } Self { stroke, fill }
} }
pub fn lerp(&self, other: &Self, time: f64) -> Self {
Self {
fill: self.fill.lerp(&other.fill, time),
stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) {
(Some(a), Some(b)) => Some(a.lerp(b, time)),
(Some(a), None) => {
if time < 0.5 {
Some(a.clone())
} else {
None
}
}
(None, Some(b)) => {
if time < 0.5 {
Some(b.clone())
} else {
None
}
}
(None, None) => None,
},
}
}
/// Get the current path's [Fill]. /// Get the current path's [Fill].
/// ///
/// # Example /// # Example
@@ -690,25 +643,6 @@ impl PathStyle {
self.stroke.clone() self.stroke.clone()
} }
/// Replace the path's [Fill] with a provided one.
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Fill, PathStyle};
/// # use core_types::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(*style.fill(), Fill::None);
///
/// let fill = Fill::solid(Color::RED);
/// style.set_fill(fill.clone());
///
/// assert_eq!(*style.fill(), fill);
/// ```
pub fn set_fill(&mut self, fill: Fill) {
self.fill = fill;
}
pub fn set_stroke_transform(&mut self, transform: DAffine2) { pub fn set_stroke_transform(&mut self, transform: DAffine2) {
if let Some(stroke) = &mut self.stroke { if let Some(stroke) = &mut self.stroke {
stroke.transform = transform; stroke.transform = transform;
+1 -1
View File
@@ -9,7 +9,7 @@ use vector_types::GradientStops;
/// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn create_artboard<T: IntoGraphicList + 'n>( pub async fn create_artboard<T: IntoGraphicList>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl ExtractAll + CloneVarArgs + Ctx,
/// Graphics to include within the artboard. /// Graphics to include within the artboard.
#[implementations( #[implementations(
+6 -6
View File
@@ -565,7 +565,7 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`. /// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired. /// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn to_graphic<T: IntoGraphicList + 'n>( pub async fn to_graphic<T: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -620,7 +620,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content. /// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))] #[node_macro::node(category("Vector"))]
pub async fn flatten_vector<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let mut output: List<Vector> = graphic_list.clone().into_flattened_list(); let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
@@ -653,25 +653,25 @@ pub async fn flatten_vector<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx,
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
pub async fn flatten_raster<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> { pub async fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
content.into_flattened_list() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn flatten_color<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> { pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
content.into_flattened_list() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. /// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn flatten_gradient<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> { pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
content.into_flattened_list() content.into_flattened_list()
} }
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1. /// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
fn colors_to_gradient<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> List<GradientStops> { fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> List<GradientStops> {
let colors = colors.into_flattened_list::<Color>(); let colors = colors.into_flattened_list::<Color>();
let total_colors = colors.len(); let total_colors = colors.len();
+36 -27
View File
@@ -1,21 +1,22 @@
use core_types::list::{Item, List}; use core_types::list::{ATTR_FILL, Item, List};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, BlendMode, Color, ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, BlendMode, Color,
Ctx, Ctx,
}; };
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::vector_types::gradient::{Gradient, GradientSpreadMethod, GradientType}; use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphic_types::vector_types::vector::style::Fill;
use graphic_types::{Graphic, Vector}; use graphic_types::{Graphic, Vector};
use linesweeper::topology::Topology; use linesweeper::topology::Topology;
use linesweeper::{BinaryOp, FillRule, binary_op}; use linesweeper::{BinaryOp, FillRule, binary_op};
use smallvec::SmallVec; use smallvec::SmallVec;
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
pub use vector_types::vector::misc::BooleanOperation; pub use vector_types::vector::misc::BooleanOperation;
use vector_types::vector::style::Fill;
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls, // TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
// TODO: since before we used a Vec of single-item `List`s and now we use a single `List` // TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
@@ -23,7 +24,7 @@ pub use vector_types::vector::misc::BooleanOperation;
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. /// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category("Vector: Modifier"), memoize)] #[node_macro::node(category("Vector: Modifier"), memoize)]
async fn boolean_operation<I: graphic_types::IntoGraphicList + 'n + Send + Clone>( async fn boolean_operation<I: graphic_types::IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened. /// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -143,12 +144,15 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
// The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own // The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own
attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY); attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY);
bake_paint_transforms(&mut attributes, copy_from_transform);
let copy_from = vector.element(index).unwrap(); let copy_from = vector.element(index).unwrap();
let mut element = Vector { let mut element = Vector {
style: copy_from.style.clone(), style: copy_from.style.clone(),
..Default::default() ..Default::default()
}; };
// An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points // Legacy Fill fallback: An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points
if let Fill::Gradient(gradient) = element.style.fill_mut() if let Fill::Gradient(gradient) = element.style.fill_mut()
&& gradient.absolute && gradient.absolute
{ {
@@ -204,15 +208,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
let mut element = Vector::from_subpath(subpath); let element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
Item::new_from_element(element) let mut item = Item::new_from_element(element)
.with_attribute(ATTR_BLEND_MODE, blend_mode) .with_attribute(ATTR_BLEND_MODE, blend_mode)
.with_attribute(ATTR_OPACITY, opacity) .with_attribute(ATTR_OPACITY, opacity)
.with_attribute(ATTR_OPACITY_FILL, fill) .with_attribute(ATTR_OPACITY_FILL, fill)
.with_attribute(ATTR_CLIPPING_MASK, clip) .with_attribute(ATTR_CLIPPING_MASK, clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
item
}; };
// Apply the parent graphic's transform to each raster element, preserving each item's layer // Apply the parent graphic's transform to each raster element, preserving each item's layer
@@ -236,15 +241,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
let mut element = Vector::from_subpath(subpath); let element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
Item::new_from_element(element) let mut item = Item::new_from_element(element)
.with_attribute(ATTR_BLEND_MODE, blend_mode) .with_attribute(ATTR_BLEND_MODE, blend_mode)
.with_attribute(ATTR_OPACITY, opacity) .with_attribute(ATTR_OPACITY, opacity)
.with_attribute(ATTR_OPACITY_FILL, fill) .with_attribute(ATTR_OPACITY_FILL, fill)
.with_attribute(ATTR_CLIPPING_MASK, clip) .with_attribute(ATTR_CLIPPING_MASK, clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
item
}; };
// Apply the parent graphic's transform to each raster element, preserving each item's layer // Apply the parent graphic's transform to each raster element, preserving each item's layer
@@ -278,9 +284,10 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
Graphic::Color(color) => color Graphic::Color(color) => color
.into_iter() .into_iter()
.map(|row| { .map(|row| {
let (color, attributes) = row.into_parts(); let (color, mut attributes) = row.into_parts();
set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color));
let mut element = Vector::default(); let mut element = Vector::default();
element.style.set_fill(Fill::Solid(color));
element.style.set_stroke_transform(DAffine2::IDENTITY); element.style.set_stroke_transform(DAffine2::IDENTITY);
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
@@ -289,19 +296,21 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
Graphic::Gradient(gradient) => gradient Graphic::Gradient(gradient) => gradient
.into_iter() .into_iter()
.map(|row| { .map(|row| {
let (stops, attributes) = row.into_parts(); let (stops, mut attributes) = row.into_parts();
let mut gradient_paint = List::new_from_element(stops);
if let Some(transform) = attributes.remove::<DAffine2>(ATTR_TRANSFORM) {
gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform);
}
if let Some(gradient_type) = attributes.remove::<GradientType>(ATTR_GRADIENT_TYPE) {
gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type);
}
if let Some(spread_method) = attributes.remove::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method);
}
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
let mut element = Vector::default(); let mut element = Vector::default();
// Convert the gradient's transform to absolute endpoints, matching `From<List<GradientStops>> for Fill`
let transform = attributes.get::<DAffine2>(ATTR_TRANSFORM).cloned().unwrap_or_default();
element.style.set_fill(Fill::Gradient(Gradient {
stops,
gradient_type: attributes.get::<GradientType>(ATTR_GRADIENT_TYPE).cloned().unwrap_or_default(),
spread_method: attributes.get::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).cloned().unwrap_or_default(),
start: transform.transform_point2(DVec2::ZERO),
end: transform.transform_point2(DVec2::X),
absolute: true,
transform: DAffine2::IDENTITY,
}));
element.style.set_stroke_transform(DAffine2::IDENTITY); element.style.set_stroke_transform(DAffine2::IDENTITY);
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
+343 -83
View File
@@ -3,22 +3,24 @@ use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, ListDyn}; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll, ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs,
OwnedContextImpl, Color, Context, Ctx, ExtractAll, OwnedContextImpl,
}; };
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, fill_graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, set_paint_attribute_at, stroke_graphic_list_at};
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Graphic, IntoGraphicList}; use graphic_types::{Graphic, IntoGraphicList};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
use vector_types::subpath::{BezierHandles, ManipulatorGroup}; use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::vector::PointDomain; use vector_types::vector::PointDomain;
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath}; use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
@@ -29,14 +31,17 @@ use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
}; };
use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::{GradientSpreadMethod, GradientType};
/// Implemented for types that contain vector items reachable via mutable access. /// Implemented for types that contain vector items reachable via mutable access.
/// Used for the fill and stroke nodes so they can apply to either `List<Graphic>` or `List<Vector>`. /// Used for the fill and stroke nodes so they can apply to either `List<Graphic>` or `List<Vector>`.
trait VectorListIterMut { trait VectorListIterMut {
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2)); fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
fn for_each_vector_list_mut(&mut self, f: impl FnMut(&mut List<Vector>));
fn vector_count(&self) -> usize; fn vector_count(&self) -> usize;
} }
@@ -51,6 +56,14 @@ impl VectorListIterMut for List<Graphic> {
} }
} }
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
for graphic in self.iter_element_values_mut() {
if let Some(vector_list) = graphic.as_vector_mut() {
f(vector_list);
};
}
}
fn vector_count(&self) -> usize { fn vector_count(&self) -> usize {
self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum() self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum()
} }
@@ -64,6 +77,10 @@ impl VectorListIterMut for List<Vector> {
} }
} }
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
f(self);
}
fn vector_count(&self) -> usize { fn vector_count(&self) -> usize {
self.len() self.len()
} }
@@ -109,26 +126,29 @@ where
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
let mut i: usize = 0; let mut i: usize = 0;
content.for_each_vector_mut(|vector, _transform| { content.for_each_vector_list_mut(|vector_list| {
let factor = match randomize { for index in 0..vector_list.len() {
true => rng.random::<f64>(), let factor = match randomize {
false => match repeat_every { true => rng.random::<f64>(),
0 => i as f64 / (length - 1).max(1) as f64, false => match repeat_every {
1 => 0., 0 => i as f64 / (length - 1).max(1) as f64,
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64, 1 => 0.,
}, _ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
}; },
};
let color = gradient.evaluate(factor); let color = gradient.evaluate(factor);
let paint = List::new_from_element(color).into_graphic_list();
if fill { if fill {
vector.style.set_fill(Fill::Solid(color)); set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
}
if stroke && vector_list.element(index).and_then(|vector| vector.style.stroke()).is_some() {
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
}
i += 1;
} }
if stroke && let Some(stroke) = vector.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector.style.set_stroke(stroke);
}
i += 1;
}); });
content content
@@ -136,41 +156,77 @@ where
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<F: Into<Fill> + 'n + Send, V: VectorListIterMut + 'n + Send>( async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
#[implementations( #[implementations(
List<Vector>, List<Vector>, List<Vector>, List<Vector>, List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>,
List<Vector>,
List<Vector>,
List<Graphic>,
List<Graphic>,
List<Graphic>,
List<Graphic>,
)] )]
mut content: V, mut content: V,
/// The fill to paint the path with. /// The fill to paint the path with.
#[default(Color::BLACK)] #[default(Color::BLACK)]
#[implementations( #[implementations(
Fill, List<Graphic>, List<Vector>, List<Color>, List<GradientStops>, List<Raster<CPU>>, List<Raster<GPU>>,
List<Color>, List<Graphic>, List<Vector>, List<Color>, List<GradientStops>, List<Raster<CPU>>, List<Raster<GPU>>,
List<GradientStops>,
Gradient,
Fill,
List<Color>,
List<GradientStops>,
Gradient,
)] )]
fill: F, mut fill: F,
_backup_color: List<Color>, _backup_color: List<Color>,
_backup_gradient: Gradient, _backup_gradient: List<GradientStops>,
_gradient_type: GradientType,
_spread_method: GradientSpreadMethod,
_transform: Option<DAffine2>,
) -> V { ) -> V {
let fill: Fill = fill.into(); if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::<List<GradientStops>>() {
content.for_each_vector_mut(|vector, _transform| { if gradient.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_none() {
vector.style.set_fill(fill.clone()); for value in gradient.iter_attribute_values_mut_or_default::<GradientType>(ATTR_GRADIENT_TYPE) {
}); *value = _gradient_type;
}
}
if gradient.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
*value = _spread_method;
}
}
if gradient.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_none() {
let transform = _transform.unwrap_or_else(|| {
// Construct a transform that covers the bounding box of the paint target
let mut bounds: Option<[DVec2; 2]> = None;
content.for_each_vector_mut(|vector, _| {
if let Some([min, max]) = vector.bounding_box() {
bounds = Some(match bounds {
Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)],
None => [min, max],
});
}
});
// Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box`
let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
if max.x - min.x < 1e-10 {
max.x = min.x + 1.;
}
if max.y - min.y < 1e-10 {
max.y = min.y + 1.;
}
initial_gradient_transform_for_bounding_box([min, max])
});
for value in gradient.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*value = transform;
}
}
}
let fill = fill.into_graphic_list();
content.for_each_vector_list_mut(|vector_list| {
// Broadcast the same paint to every item, scanning the attribute column once instead of per index
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(ATTR_FILL) {
*slot = fill.clone();
}
});
content content
} }
@@ -195,14 +251,41 @@ impl IntoF64Vec for String {
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<V, L: IntoF64Vec>( async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
#[implementations(List<Vector>, List<Vector>, List<Vector>, List<Graphic>, List<Graphic>, List<Graphic>)] #[implementations(
List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Vector>, List<Vector>,
List<Vector>, List<Vector>, List<Vector>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Graphic>, List<Graphic>, List<Graphic>,
)]
mut content: List<V>, mut content: List<V>,
/// The stroke color. /// The stroke paint.
#[default(Color::BLACK)] #[default(Color::BLACK)]
color: List<Color>, #[implementations(
List<Graphic>, List<Graphic>, List<Graphic>,
List<Vector>, List<Vector>, List<Vector>,
List<Color>, List<Color>, List<Color>,
List<GradientStops>, List<GradientStops>, List<GradientStops>,
List<Raster<CPU>>, List<Raster<CPU>>, List<Raster<CPU>>,
List<Raster<GPU>>, List<Raster<GPU>>, List<Raster<GPU>>,
List<Graphic>, List<Graphic>, List<Graphic>,
List<Vector>, List<Vector>, List<Vector>,
List<Color>, List<Color>, List<Color>,
List<GradientStops>, List<GradientStops>, List<GradientStops>,
List<Raster<CPU>>, List<Raster<CPU>>, List<Raster<CPU>>,
List<Raster<GPU>>, List<Raster<GPU>>, List<Raster<GPU>>,
)]
paint: P,
/// The stroke thickness. /// The stroke thickness.
#[unit(" px")] #[unit(" px")]
#[default(2.)] #[default(2.)]
@@ -220,7 +303,20 @@ async fn stroke<V, L: IntoF64Vec>(
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke. /// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
paint_order: PaintOrder, paint_order: PaintOrder,
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed. /// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
#[implementations(List<f64>, f64, String, List<f64>, f64, String)] #[implementations(
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
List<f64>, f64, String,
)]
dash_lengths: L, dash_lengths: L,
/// The phase offset distance from the starting point of the dash pattern. /// The phase offset distance from the starting point of the dash pattern.
#[unit(" px")] #[unit(" px")]
@@ -232,7 +328,8 @@ where
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
let stroke = Stroke { let stroke = Stroke {
color: color.element(0).copied(), // TODO: Remove once the deprecated `Stroke.color` field is deleted in favor of the `ATTR_STROKE` attribute
color: None,
weight, weight,
dash_lengths, dash_lengths,
dash_offset, dash_offset,
@@ -250,6 +347,13 @@ where
vector.style.set_stroke(stroke); vector.style.set_stroke(stroke);
}); });
let paint = paint.into_graphic_list();
content.for_each_vector_list_mut(|vector_list| {
// Broadcast the same paint to every item, scanning the attribute column once instead of per index
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(ATTR_STROKE) {
*slot = paint.clone();
}
});
content content
} }
@@ -1156,15 +1260,21 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
// TODO: Make this node support stroke align, which it currently ignores // TODO: Make this node support stroke align, which it currently ignores
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let flattened: List<Vector> = graphic_list.clone().into_flattened_list(); let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
// A fill exists when the canonical attribute carries paint or, matching the renderer's fallback, when the legacy `style.fill` does
let has_fills: Vec<bool> = (0..flattened.len())
.map(|index| has_paint_at(&flattened, index, ATTR_FILL) || flattened.element(index).is_some_and(|vector| !vector.style.fill().is_none()))
.collect();
let mut output: List<Vector> = flattened let mut output: List<Vector> = flattened
.into_iter() .into_iter()
.flat_map(|row| { .zip(has_fills)
.flat_map(|(row, has_fill)| {
let (mut vector, attributes) = row.into_parts(); let (mut vector, attributes) = row.into_parts();
let stroke = vector.style.stroke().clone().unwrap_or_default(); let stroke = vector.style.stroke().clone().unwrap_or_default();
@@ -1197,7 +1307,7 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
let stroke_options_default = kurbo::StrokeOpts::default(); let stroke_options_default = kurbo::StrokeOpts::default();
let stroke_options_stable = kurbo::StrokeOpts::default().stable_dash_order(true); let stroke_options_stable = kurbo::StrokeOpts::default().stable_dash_order(true);
// 0.25 is balanced between performace and accuracy of the curve. // 0.25 is balanced between performance and accuracy of the curve.
const STROKE_TOLERANCE: f64 = 0.25; const STROKE_TOLERANCE: f64 = 0.25;
for mut path in bezpaths { for mut path in bezpaths {
@@ -1214,14 +1324,7 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
solidified_stroke.append_bezpath(solidified); solidified_stroke.append_bezpath(solidified);
} }
// We set the solidified stroke's fill to the stroke's color and without a stroke.
if let Some(stroke) = vector.style.stroke() {
solidified_stroke.style.set_fill(Fill::solid_or_none(stroke.color));
}
// If the original vector has a fill, preserve it as a separate item with the stroke cleared. // If the original vector has a fill, preserve it as a separate item with the stroke cleared.
let has_attr_fill = attributes.keys().any(|k| k == ATTR_FILL);
let has_fill = has_attr_fill || !vector.style.fill().is_none();
let fill_row = has_fill.then(|| { let fill_row = has_fill.then(|| {
vector.style.clear_stroke(); vector.style.clear_stroke();
let mut fill_attributes = attributes.clone(); let mut fill_attributes = attributes.clone();
@@ -1235,6 +1338,13 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL); stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
// Fall back to the legacy stroke color when no canonical stroke paint attribute was carried over
if !stroke_attributes.get::<List<Graphic>>(ATTR_FILL).is_some_and(is_paint_present)
&& let Some(color) = stroke.color
{
set_paint_attribute(&mut stroke_attributes, ATTR_FILL, List::new_from_element(color));
}
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
// Ordering based on the paint order. The first item in the `List` is rendered below the second. // Ordering based on the paint order. The first item in the `List` is rendered below the second.
@@ -1331,12 +1441,14 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List<Vec
// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes.
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let flattened = graphic_list.clone().into_flattened_list::<Vector>(); let flattened = graphic_list.clone().into_flattened_list::<Vector>();
// Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to // Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to
let mut output_list = List::new_from_element(Vector::default()); let mut output_list = List::new_from_element(Vector::default());
let mut primary_source = None;
let output = output_list.element_mut(0).unwrap(); let output = output_list.element_mut(0).unwrap();
// Concatenate every vector element's subpaths into the single output compound path // Concatenate every vector element's subpaths into the single output compound path
@@ -1349,11 +1461,30 @@ pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[impleme
(index, node_id).hash(&mut hasher); (index, node_id).hash(&mut hasher);
let collision_hash_seed = hasher.finish(); let collision_hash_seed = hasher.finish();
output.concat(element, flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index), collision_hash_seed); let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
output.concat(element, source_transform, collision_hash_seed);
// TODO: Make this instead use the first encountered style // TODO: Make this instead use the first encountered style
// Use the last encountered style as the output style // Use the last encountered style as the output style
output.style = element.style.clone(); output.style = element.style.clone();
primary_source = Some((index, source_transform));
}
if let Some((primary, source_transform)) = primary_source {
let source_attributes = flattened.clone_item_attributes(primary);
let mut attributes = ItemAttributeValues::new();
attributes.insert_cloned_from(&source_attributes, ATTR_FILL);
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE);
bake_paint_transforms(&mut attributes, source_transform);
let output = std::mem::take(output_list.element_mut(0).unwrap());
output_list = List::new_from_item(Item::from_parts(output, attributes));
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
let layer_path: List<NodeId> = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary);
output_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
} }
// Preserve a reference to the original upstream `List<Graphic>` so the renderer can recurse into it // Preserve a reference to the original upstream `List<Graphic>` so the renderer can recurse into it
@@ -1361,13 +1492,6 @@ pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[impleme
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge. // This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
if !flattened.is_empty() {
let primary = flattened.len() - 1;
let layer_path: List<NodeId> = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary);
output_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
}
output_list output_list
} }
@@ -2064,7 +2188,7 @@ async fn offset_points(
/// ///
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn morph<I: IntoGraphicList + 'n + Send + Clone>( async fn morph<I: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. /// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -2177,6 +2301,79 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
} }
} }
fn lerp_gradient_transform(gradient_list_a: &List<GradientStops>, gradient_list_b: &List<GradientStops>, time: f64) -> DAffine2 {
let transform_a = gradient_list_a.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
let transform_b = gradient_list_b.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
let start_a = transform_a.translation;
let end_a = transform_a.translation + transform_a.matrix2.x_axis;
let start_b = transform_b.translation;
let end_b = transform_b.translation + transform_b.matrix2.x_axis;
let start = start_a.lerp(start_b, time);
let end = end_a.lerp(end_b, time);
let metadata_source_transform = if time < 0.5 { transform_a } else { transform_b };
build_transform_with_y_preservation(metadata_source_transform, start, end)
}
// Lerp between two graphics. Solid color and gradient pairings interpolate; all other pairings step at the midpoint.
fn lerp_graphic(a: Option<&List<Graphic>>, b: Option<&List<Graphic>>, time: f64) -> Option<List<Graphic>> {
let transparent = List::new_from_element(Color::TRANSPARENT).into_graphic_list();
let a = a.filter(|graphic_list| is_paint_present(graphic_list));
let b = b.filter(|graphic_list| is_paint_present(graphic_list));
let (a, b) = match (a, b) {
(None, None) => return None,
(Some(a), None) => (a, &transparent),
(None, Some(b)) => (&transparent, b),
(Some(a), Some(b)) => (a, b),
};
// This keeps the gradient metadata attributes
let gradient_with_stops = |mut gradient_list: List<GradientStops>, stops: GradientStops| -> Graphic {
if let Some(target) = gradient_list.element_mut(0) {
*target = stops;
} else {
gradient_list.push(Item::new_from_element(stops));
}
Graphic::Gradient(gradient_list)
};
let graphic = match (a.element(0), b.element(0)) {
(Some(Graphic::Color(color_list_a)), Some(Graphic::Color(color_list_b))) => color_list_a
.element(0)
.zip(color_list_b.element(0))
.map(|(color_a, color_b)| Graphic::from(color_a.lerp(color_b, time as f32))),
(Some(Graphic::Color(color_list_a)), Some(Graphic::Gradient(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| {
let mut solid_to_gradient = stops_b.clone();
solid_to_gradient.color.iter_mut().for_each(|color| *color = *color_a);
let stops = solid_to_gradient.lerp(stops_b, time);
gradient_with_stops(gradient_list_b.clone(), stops)
}),
(Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Color(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| {
let mut gradient_to_solid = stops_a.clone();
gradient_to_solid.color.iter_mut().for_each(|color| *color = *color_b);
let stops = stops_a.lerp(&gradient_to_solid, time);
gradient_with_stops(gradient_list_a.clone(), stops)
}),
(Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Gradient(gradient_list_b))) => gradient_list_a.element(0).zip(gradient_list_b.element(0)).map(|(stops_a, stops_b)| {
let stops = stops_a.lerp(stops_b, time);
let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
let mut gradient_list = metadata_source.clone();
gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
gradient_with_stops(gradient_list, stops)
}),
// Pairings beyond solid colors and gradients (raster, vector, or mixed) can't be interpolated, so step at the midpoint
_ => return Some(if time < 0.5 { a.clone() } else { b.clone() }),
};
graphic.map(List::new_from_element)
}
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools. // Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
let mut graphic_list_content = content.clone().into_graphic_list(); let mut graphic_list_content = content.clone().into_graphic_list();
@@ -2435,9 +2632,35 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes)); return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
} }
let mut vector = Vector { let mut vector = Vector::default();
style: source_element.style.lerp(&target_element.style, time), vector.style.stroke = match (source_element.style.stroke.as_ref(), target_element.style.stroke.as_ref()) {
..Default::default() (Some(a), Some(b)) => Some(a.lerp(b, time)),
(Some(a), None) => {
if time < 0.5 {
Some(a.clone())
} else {
None
}
}
(None, Some(b)) => {
if time < 0.5 {
None
} else {
Some(b.clone())
}
}
(None, None) => None,
};
let fill_paint = {
let source = fill_graphic_list_at(&content, source_index);
let target = fill_graphic_list_at(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time)
};
let stroke_paint = {
let source = stroke_graphic_list_at(&content, source_index);
let target = stroke_graphic_list_at(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time)
}; };
// Work directly with manipulator groups, bypassing the BezPath intermediate representation. // Work directly with manipulator groups, bypassing the BezPath intermediate representation.
@@ -2588,16 +2811,23 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
let primary_index = if time < 0.5 { source_index } else { target_index }; let primary_index = if time < 0.5 { source_index } else { target_index };
let layer_path: List<NodeId> = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index); let layer_path: List<NodeId> = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index);
List::new_from_item( let mut item = Item::new_from_element(vector)
Item::new_from_element(vector) .with_attribute(ATTR_TRANSFORM, lerped_transform)
.with_attribute(ATTR_TRANSFORM, lerped_transform) .with_attribute(ATTR_BLEND_MODE, lerped_blend_mode)
.with_attribute(ATTR_BLEND_MODE, lerped_blend_mode) .with_attribute(ATTR_OPACITY, lerped_opacity)
.with_attribute(ATTR_OPACITY, lerped_opacity) .with_attribute(ATTR_OPACITY_FILL, lerped_fill)
.with_attribute(ATTR_OPACITY_FILL, lerped_fill) .with_attribute(ATTR_CLIPPING_MASK, lerped_clip)
.with_attribute(ATTR_CLIPPING_MASK, lerped_clip) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path) .with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content),
) if let Some(fill) = fill_paint {
item.set_attribute(ATTR_FILL, fill);
}
if let Some(stroke) = stroke_paint {
item.set_attribute(ATTR_STROKE, stroke);
}
List::new_from_item(item)
} }
fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector { fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector {
@@ -3207,6 +3437,36 @@ mod test {
assert!((morphed.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3); assert!((morphed.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3);
} }
#[tokio::test]
async fn morph_interpolates_fill() {
let rect = || {
let mut v = Vector::default();
v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
v
};
let item_a = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
.with_attribute(ATTR_FILL, List::new_from_element(Color::RED).into_graphic_list());
let item_b = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into()))
.with_attribute(ATTR_FILL, List::new_from_element(Color::BLUE).into_graphic_list());
let mut content = List::new_from_item(item_a);
content.push(item_b);
let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await;
let fill = fill_graphic_list_at(&morphed, 0).expect("Morph should keep the fill paint at the midpoint");
// Interpolated color between red and blue should have >0 value on both R and B
let Some(Graphic::Color(colors)) = fill.element(0) else {
panic!("Expected a solid color fill, got {:?}", fill.element(0));
};
let color = *colors.element(0).expect("Color present");
assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}");
}
#[track_caller] #[track_caller]
fn contains_segment(vector: Vector, target: PathSeg) { fn contains_segment(vector: Vector, target: PathSeg) {
let segments = vector.segment_iter().map(|x| x.1); let segments = vector.segment_iter().map(|x| x.1);