Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::messages::prelude::*;
use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::list::List;
use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
Expand Down Expand Up @@ -226,8 +227,11 @@ pub enum DocumentMessage {
UpdateClipTargets {
clip_targets: HashSet<NodeId>,
},
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
// `UpdateVectorData` is 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)]
UpdateVectorData {
vector_data: HashMap<NodeId, Arc<Vector>>,
vector_data: HashMap<NodeId, Arc<List<Vector>>>,
},
Undo,
UngroupSelectedLayers,
Expand Down
36 changes: 28 additions & 8 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ use glam::{DAffine2, DVec2};
use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::Graphic;
use graphene_std::graphic::{color_to_graphic_list, fill_to_graphic_list};
use graphene_std::list::{ATTR_FILL_GRAPHIC, ATTR_STROKE_PAINT_GRAPHIC, List};
use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode;
Expand All @@ -40,8 +43,9 @@ use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::{Fill, RenderMode};
use graphene_std::vector::style::{RenderMode, Stroke};
use kurbo::{Affine, BezPath, Line, PathSeg};
use std::borrow::Cow;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
Expand Down Expand Up @@ -2384,16 +2388,32 @@ impl DocumentMessageHandler {
let mut resulting_layers: Vec<NodeId> = Vec::new();

for layer in selected_layers {
let style = self.network_interface.document_metadata().layer_vector_data.get(&layer).map(|arc| arc.style.clone());
let Some(style) = style else {
let vector_list = self.network_interface.document_metadata().layer_vector_data.get(&layer).cloned();
let Some(vector_list) = vector_list else {
resulting_layers.push(layer.to_node());
continue;
};

let has_fill = !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());
let style = vector_list.element(0).map(|vector| &vector.style);

let fill_graphic_list = vector_list
.attribute::<List<Graphic>>(ATTR_FILL_GRAPHIC, 0)
.filter(|list| !list.is_empty())
.map(Cow::Borrowed)
.or_else(|| style.and_then(|style| fill_to_graphic_list(style.fill())).map(Cow::Owned));
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));

let stroke_paint_graphic_list = vector_list
.attribute::<List<Graphic>>(ATTR_STROKE_PAINT_GRAPHIC, 0)
.filter(|list| !list.is_empty())
.map(Cow::Borrowed)
.or_else(|| color_to_graphic_list(style.and_then(|style| style.stroke().and_then(|s| s.color()))).map(Cow::Owned));
let stroke_paint_graphic = stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0));

let has_fill = fill_graphic.is_some();

let stroke_renderable = style.is_some_and(|s| s.stroke.as_ref().is_some_and(Stroke::has_renderable_stroke));
let stroke_paint_visible = stroke_paint_graphic.is_some_and(|g| !g.is_fully_transparent());
let has_stroke = stroke_renderable && stroke_paint_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,7 @@ 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::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
Expand Down Expand Up @@ -38,7 +39,7 @@ pub struct DocumentMetadata {
pub vector_modify: HashMap<NodeId, Vector>,
/// 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>>,
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<List<Vector>>>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}
Expand Down Expand Up @@ -225,7 +226,7 @@ impl DocumentMetadata {
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
/// bounds for non-vector layers (groups, raster, text, color, gradient).
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
if let Some(vector) = self.layer_vector_data.get(&layer)
if let Some(vector) = self.layer_vector_data.get(&layer).and_then(|vector_list| vector_list.element(0))
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer))
{
return Some(bounds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use graph_craft::Type;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graphene_std::ContextDependencies;
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 @@ -3230,8 +3231,9 @@ impl NodeNetworkInterface {
}
return Some(modified);
}

self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone())
// Only item 0 is returned since editing tools can only target a single item currently.
let vector_list = self.document_metadata.layer_vector_data.get(&layer).cloned();
vector_list.and_then(|list| list.element(0).cloned())
}

/// The vector geometry an upstream Path node would surface for editing.
Expand Down Expand Up @@ -3393,7 +3395,7 @@ impl NodeNetworkInterface {
}

/// Update the layer vector data (for layers without Path nodes)
pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>) {
pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap<LayerNodeIdentifier, Arc<List<Vector>>>) {
self.document_metadata.layer_vector_data = new_layer_vector_data;
}
}
Expand Down
6 changes: 6 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";

/// List<Graphic> data for fill.
pub const ATTR_FILL_GRAPHIC: &str = "fill_graphic";

/// List<Graphic> data for stroke.
pub const ATTR_STROKE_PAINT_GRAPHIC: &str = "stroke_paint_graphic";

// ========================
// TRAIT: AnyAttributeValue
// ========================
Expand Down
135 changes: 132 additions & 3 deletions node-graph/libraries/graphic-types/src/graphic.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::borrow::Cow;

use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::List;
use core_types::list::{ATTR_STROKE_PAINT_GRAPHIC, Item, List};
use core_types::ops::ListConvert;
use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::DAffine2;
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
// use vector_types::Vector;

pub use vector_types::Vector;
use vector_types::vector::style::Fill;

/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
Expand Down Expand Up @@ -169,6 +172,30 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
output
}

/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
match fill {
Fill::None => None,
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
Fill::Gradient(gradient) => {
let gradient_row = Item::new_from_element(gradient.stops.clone())
.with_attribute(ATTR_TRANSFORM, gradient.to_transform())
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
let gradient_list = List::new_from_item(gradient_row);

Some(List::new_from_element(Graphic::Gradient(gradient_list)))
}
}
}

/// Converts a `Color` into the `List<Graphic>` representation used as paint storage.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub fn color_to_graphic_list(color: Option<Color>) -> Option<List<Graphic>> {
color.as_ref().map(|color| List::new_from_element((*color).into()))
}

/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized {
Expand Down Expand Up @@ -332,11 +359,37 @@ impl Graphic {
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
opacity > 1. - f64::EPSILON && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
let stroke_paint_graphic_list = vector
.attribute::<List<Graphic>>(ATTR_STROKE_PAINT_GRAPHIC, index)
.filter(|list| !list.is_empty())
.map(Cow::Borrowed)
.or_else(|| color_to_graphic_list(element.style.stroke().and_then(|s| s.color())).map(Cow::Owned));
let stroke_paint_graphic = stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0));

opacity > 1. - f64::EPSILON
&& element.style.fill().is_opaque()
&& (element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || stroke_paint_graphic.is_none_or(|graphic| graphic.is_fully_transparent()))
}),
_ => false,
}
}

pub fn is_opaque(&self) -> bool {
match self {
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.a() >= 1. - f32::EPSILON)),
_ => false,
}
}

pub fn is_fully_transparent(&self) -> bool {
match self {
Self::Color(list) => list.element(0).is_some_and(|c| c.a() == 0.),
Self::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
// FIXME: Write recursive check for other types
_ => false,
}
}
}

impl BoundingBox for Graphic {
Expand Down Expand Up @@ -462,3 +515,79 @@ impl<T: Clone> OmitIndex for List<T> {
self.omit_index(self.len() - index)
}
}

#[cfg(test)]
mod graphic_is_opaque_tests {
use vector_types::{GradientSpreadMethod, GradientStop};

use super::*;

fn color_graphic(alpha: f64) -> Graphic {
let color = Color::from_rgbaf32(1.0, 0.0, 0.0, alpha as f32).unwrap();
Graphic::Color(List::new_from_element(color))
}

fn gradient_graphic(gradient: GradientStops) -> Graphic {
let mut gradient_list = List::new_from_element(gradient);
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad);
Graphic::Gradient(gradient_list)
}

#[test]
fn opaque_color_is_opaque() {
let g = color_graphic(1.0);
assert!(g.is_opaque());
}

#[test]
fn transparent_color_is_not_opaque() {
let g = color_graphic(0.5);
assert!(!g.is_opaque());
}

#[test]
fn vector_is_not_opaque() {
let g = Graphic::Vector(List::default());
assert!(!g.is_opaque());
}

#[test]
fn gradient_with_all_opaque_stops_is_opaque() {
let color_1 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let color_2 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(g.is_opaque());
}

#[test]
fn gradient_with_transparent_stop_is_not_opaque() {
let color_1 = Color::from_rgbaf32(1.0, 0.0, 0.0, 0.5).unwrap();
let color_2 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(!g.is_opaque());
}
}
Loading
Loading