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
@@ -5,7 +5,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DVec2, IVec2};
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
use graph_craft::descriptor;
@@ -15,7 +14,7 @@ use graphene_std::ProtoNodeIdentifier;
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId;
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
use graphene_std::vector::style::{Fill, PaintOrder, StrokeAlign};
use std::collections::HashMap;
use std::f64::consts::PI;
use std::ops::Range;
@@ -1122,9 +1121,6 @@ pub fn document_migration_replace_resources_referenced_by_hash(document_serializ
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
document.network_interface.migrate_path_modify_node();
// Legacy `Fill::Gradient`s are converted to absolute by the deferred migration pre-pass that measures each fill's geometry
document.pending_gradient_migration = !graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).is_empty();
let network = document.network_interface.document_network().clone();
// Apply string and node replacements to each node
@@ -1575,6 +1571,96 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
inputs_count = 5;
}
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the
// value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform).
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
// Content: no change
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
// Fill: a literal Fill value is decomposed, and a wired input (`List<GradientStops> / List<Color>`) is kept as-is
match old_inputs[1].as_value() {
Some(TaggedValue::Fill(old_fill)) => {
let exposed = old_inputs[1].is_exposed();
let fill_value = match old_fill {
Fill::None => TaggedValue::Color(None),
Fill::Solid(color) => TaggedValue::Color(Some(*color)),
Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
// Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults
if let Fill::Gradient(gradient) = old_fill {
document.network_interface.set_input(
&InputConnector::node(*node_id, 4),
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
network_path,
);
let transform = if gradient.absolute {
Some(gradient.transform * gradient.to_transform())
} else {
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint target's bounds
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, gradient.clone()));
None
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
}
}
// Wired/exposed fill keeps the connection.
// The generic paint connector accepts `List<Color>`/`List<GradientStops>` sources directly, and there were no other nodes which can generate output type that implements `From` for `Fill`.
_ => {
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
}
}
// Color backup: no change
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
// Gradient backup: extract stops
if let Some(TaggedValue::FillGradient(g)) = old_inputs[3].as_value() {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path);
// A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore
if matches!(old_inputs[1].as_value(), Some(TaggedValue::Fill(Fill::None | Fill::Solid(_)))) {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
NodeInput::value(TaggedValue::GradientSpreadMethod(g.spread_method), false),
network_path,
);
let transform = if g.absolute {
Some(g.transform * g.to_transform())
} else {
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone()));
None
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
}
}
inputs_count = 7;
}
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();