Add renderer support for painting vector with "fill" and "stroke" attributes with graphic List<T> types (#4111)

* Add conversion from Fill to Table<Graphic>

* Refactor Vector vello renderer for Gradient / Color

* Refactor Vector SVG renderer for Gradient / Color

* Fix conflicts

* Add basic clipping-based fill for SVG rendering

* Use Cow to avoid cloning graphic list for fill

* Cleanup for Cow usage

* format code

* Use `<pattern>` instead of `<clipPath>` for clip

This simplifies the future implementation of clipping-based rendering
for strokes, as the stroke does not support the use of a clip path but
rather paint sources from a paint server.

* Move svg pattern rendering function to RenderExt

* Fix comment

* Fix empty fill list rendering as default black

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Move opaque check function to Graphic impl

* Add color converter and debug node to use graphic

* WIP: Use List<Graphic> to render Color & Gradient

* Use `Arc<List<Vector>>` for vector_data metadata

This exposes List's attributes to message handlers, enabling them to
access the necessary attribute data such as ATTR_STROKE_PAINT_GRAPHIC
as `Fill` and `Stroke` will not have paint information in the future.

* Recurse opacity checks on nested `Graphic`

Also extracts `fill_graphic_list_at` /
`stroke_paint_graphic_list_at` to share the row-attribute
lookup across the existing call sites.

* Fix fill and stroke visibility check degradation

* Fix clipping based stroke paint positioning

* Refactor vello renderer for stroke to use graphic

* Reduce `Fill` / `Stroke.color` to `List<Graphic>` allocations

* Revert "Use `Arc<List<Vector>>` for vector_data metadata"

This reverts commit 4285243a5d

* Expose paint row attributes as dedicated metadata for vectors

Add `fill_attributes` / `stroke_paint_attributes` to `DocumentMetadata`
so the `ExpandFillStrokeOnSelectedLayers` handler can read row paint
visibility without exposing entire `List<Vector>`.

* Fix transparency check to consider fill opacity

* Fix consistency of gradient placement for SVG stroke

* Rename `stroke_paint_..` to `stroke_..`

* Remove debug nodes

* Allow to use any graphic type without casting

* Rename `fill_graphic` / `stroke_graphic` to `fill` / `stroke`

* Fix SVG pattern placement when stroke transform differs from item transform

* Fix click target fill check for empty list in graphic

* Fix blank fill/stroke attribute masking legacy style

* Fix SVG's paint order trick for vector/raster fills

* Add zero-division guard for pattern wraparound prevention

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
YohYamasaki
2026-06-11 21:50:27 +02:00
committed by Timon
parent e524dff1a7
commit 2b8ef42086
15 changed files with 976 additions and 259 deletions

View File

@@ -12,6 +12,8 @@ use crate::messages::prelude::*;
use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
@@ -235,6 +237,16 @@ pub enum DocumentMessage {
UpdateVectorData {
vector_data: HashMap<NodeId, Arc<Vector>>,
},
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
#[serde(skip)]
UpdateFillAttributes {
fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
},
#[serde(skip)]
UpdateStrokeAttributes {
stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
},
Undo,
UngroupSelectedLayers,
UngroupLayer {

View File

@@ -1404,6 +1404,34 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
.collect();
self.network_interface.update_vector_data(layer_vector_data);
}
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_fill_attributes = fill_attributes
.into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, attrs)
})
})
.collect();
self.network_interface.update_fill_attributes(layer_fill_attributes);
}
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_stroke_attributes = stroke_attributes
.into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, attrs)
})
})
.collect();
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
}
DocumentMessage::Undo => {
if self.network_interface.transaction_status() != TransactionStatus::Finished {
return;
@@ -2486,10 +2514,23 @@ impl DocumentMessageHandler {
continue;
};
let has_fill = !matches!(style.fill, Fill::None);
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 has_fill = if let Some(list) = fill_graphic_list {
list.element(0).is_some()
} else {
!matches!(style.fill, Fill::None)
};
// `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color.
// So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke.
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke());
// `ATTR_STROKE` is the source of truth when set; fall back to `style.stroke.color` only when no attribute is present.
let stroke_visible = if let Some(list) = stroke_graphic_list {
list.element(0).is_some_and(|g| !g.is_fully_transparent())
} else {
style.stroke.as_ref().and_then(|s| s.color()).is_some_and(|c| c.a() != 0.)
};
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
if !has_stroke {

View File

@@ -6,6 +6,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
@@ -39,6 +41,12 @@ pub struct DocumentMetadata {
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
/// This provides accurate SegmentIds for layers without explicit Path nodes.
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
/// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint
/// information that lives on the list.
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read
/// stroke paint information that lives on the list.
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}

View File

@@ -24,6 +24,8 @@ use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graphene_std::ContextDependencies;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::transform::Footprint;
@@ -3401,6 +3403,16 @@ impl NodeNetworkInterface {
pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>) {
self.document_metadata.layer_vector_data = new_layer_vector_data;
}
/// Update the per-layer `ATTR_FILL` snapshot.
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes;
}
/// Update the per-layer `ATTR_STROKE` snapshot.
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
}
}
// Public mutable methods

View File

@@ -446,6 +446,8 @@ impl NodeGraphExecutor {
text_frames,
clip_targets,
vector_data,
fill_attributes,
stroke_attributes,
backgrounds: _,
} = render_output.metadata;
@@ -460,6 +462,8 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
responses.add(DocumentMessage::UpdateVectorData { vector_data });
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes });
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);