Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a3390c4
Add conversion from Fill to Table<Graphic>
YohYamasaki May 6, 2026
5d894cd
Refactor Vector vello renderer for Gradient / Color
YohYamasaki May 7, 2026
09d38a4
Refactor Vector SVG renderer for Gradient / Color
YohYamasaki May 8, 2026
5e7940a
Fix conflicts
YohYamasaki May 12, 2026
d096e9f
Add basic clipping-based fill for SVG rendering
YohYamasaki May 13, 2026
31818d5
Use Cow to avoid cloning graphic list for fill
YohYamasaki May 15, 2026
094dc23
Cleanup for Cow usage
YohYamasaki May 15, 2026
7f30341
format code
YohYamasaki May 15, 2026
b1a0ac1
Use `<pattern>` instead of `<clipPath>` for clip
YohYamasaki May 18, 2026
d7be8b9
Move svg pattern rendering function to RenderExt
YohYamasaki May 19, 2026
ff1f0c6
Fix comment
YohYamasaki May 19, 2026
32775de
Fix empty fill list rendering as default black
YohYamasaki May 19, 2026
ca59f09
Move opaque check function to Graphic impl
YohYamasaki May 19, 2026
ede77ee
Add color converter and debug node to use graphic
YohYamasaki May 19, 2026
a30fe2e
WIP: Use List<Graphic> to render Color & Gradient
YohYamasaki May 19, 2026
12e8858
Use `Arc<List<Vector>>` for vector_data metadata
YohYamasaki May 20, 2026
8c51203
Recurse opacity checks on nested `Graphic`
YohYamasaki May 21, 2026
4255ab8
Fix fill and stroke visibility check degradation
YohYamasaki May 21, 2026
4023a0b
Fix clipping based stroke paint positioning
YohYamasaki May 21, 2026
8d7eca0
Refactor vello renderer for stroke to use graphic
YohYamasaki May 21, 2026
0a61c70
Reduce `Fill` / `Stroke.color` to `List<Graphic>` allocations
YohYamasaki May 22, 2026
29e2b81
Revert "Use `Arc<List<Vector>>` for vector_data metadata"
YohYamasaki May 22, 2026
44eda3f
Expose paint row attributes as dedicated metadata for vectors
YohYamasaki May 22, 2026
32bae0a
Fix transparency check to consider fill opacity
YohYamasaki May 22, 2026
d572523
Fix consistency of gradient placement for SVG stroke
YohYamasaki May 22, 2026
fda03c3
Rename `stroke_paint_..` to `stroke_..`
YohYamasaki Jun 8, 2026
907a511
Remove debug nodes
YohYamasaki Jun 8, 2026
4d67d38
Allow to use any graphic type without casting
YohYamasaki Jun 8, 2026
8e1bf00
Rename `fill_graphic` / `stroke_graphic` to `fill` / `stroke`
YohYamasaki Jun 8, 2026
d2a04df
Fix SVG pattern placement when stroke transform differs from item tra…
YohYamasaki Jun 11, 2026
f93a39e
Fix click target fill check for empty list in graphic
YohYamasaki Jun 11, 2026
6ae6860
Fix blank fill/stroke attribute masking legacy style
YohYamasaki Jun 11, 2026
812146b
Fix SVG's paint order trick for vector/raster fills
YohYamasaki Jun 11, 2026
5836e81
Add zero-division guard for pattern wraparound prevention
YohYamasaki Jun 11, 2026
46b9874
Code review
Keavon Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
45 changes: 43 additions & 2 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@ impl NodeGraphExecutor {
text_frames,
clip_targets,
vector_data,
fill_attributes,
stroke_attributes,
backgrounds: _,
} = render_output.metadata;

Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
convert_node!(from: List<NodeId>, to: AttributeValueDyn),
convert_node!(from: List<Color>, to: AttributeValueDyn),
convert_node!(from: List<GradientStops>, to: AttributeValueDyn),
convert_node!(from: List<Vector>, to: AttributeValueDyn),
convert_node!(from: List<Raster<CPU>>, to: AttributeValueDyn),
convert_node!(from: List<Raster<GPU>>, to: AttributeValueDyn),
convert_node!(from: List<Graphic>, to: AttributeValueDyn),
// into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>),
#[cfg(feature = "gpu")]
Expand Down
22 changes: 22 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method";
/// Gradient's `GradientType` (`Linear` or `Radial`).
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";

/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
pub const ATTR_FILL: &str = "fill";

/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
pub const ATTR_STROKE: &str = "stroke";

// ===========================
// Implicit attribute defaults
// ===========================
Expand Down Expand Up @@ -655,6 +661,22 @@ impl ItemAttributeValues {
}
})
}

/// Moves the attribute at `from_key` to `to_key`.
/// Does nothing if `from_key` is absent, overwrites any existing `to_key`.
pub fn rename(&mut self, from_key: &str, to_key: impl Into<String>) {
let Some(pos) = self.0.iter().position(|(k, _)| k == from_key) else { return };
let (_, value) = self.0.remove(pos);

let to_key = to_key.into();
for (existing_key, existing_value) in &mut self.0 {
if *existing_key == to_key {
*existing_value = value;
return;
}
}
self.0.push((to_key, value));
}
}

// ==========
Expand Down
4 changes: 2 additions & 2 deletions node-graph/libraries/core-types/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl<T: ToString + Send> Convert<String, ()> for T {
}

pub trait ListConvert<U> {
fn convert_row(self) -> U;
fn convert_item(self) -> U;
}

impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
Expand All @@ -65,7 +65,7 @@ impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
.into_iter()
.map(|row| {
let (element, attributes) = row.into_parts();
Item::from_parts(element.convert_row(), attributes)
Item::from_parts(element.convert_item(), attributes)
})
.collect();
list
Expand Down
2 changes: 1 addition & 1 deletion node-graph/libraries/graphic-types/src/artboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use glam::DAffine2;

/// Nominal wrapper around `List<Graphic>` representing a single artboard's content.
///
/// Per-artboard metadata (location, dimensions, background, clip) lives as row attributes on the
/// Per-artboard metadata (location, dimensions, background, clip) lives as attributes on the
/// enclosing `List<Artboard>`, not as fields here. This keeps `Artboard` a pure type-system boundary
/// that prevents arbitrary `List<List<...<Graphic>>>` nesting.
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
Expand Down
Loading
Loading