diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 7cffbf2cf3..8c68f89aa7 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -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; @@ -226,8 +227,11 @@ pub enum DocumentMessage { UpdateClipTargets { clip_targets: HashSet, }, + // `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>, + vector_data: HashMap>>, }, Undo, UngroupSelectedLayers, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f987e28146..3096345f87 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -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; @@ -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; @@ -2384,17 +2388,32 @@ impl DocumentMessageHandler { let mut resulting_layers: Vec = 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 style = vector_list.element(0).map(|vector| &vector.style); - 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. - // FIXME: Consider if we need to check ATTR_STROKE_PAINT_GRAPHIC - let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()); + let fill_graphic_list = vector_list + .attribute::>(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::>(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 { diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 496fa909c8..534af08e01 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -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; @@ -38,7 +39,7 @@ pub struct DocumentMetadata { pub vector_modify: HashMap, /// 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>, + pub layer_vector_data: HashMap>>, /// Transform from document space to viewport space. pub document_to_viewport: DAffine2, } @@ -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); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 0253f707f6..d4bb99ba05 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -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; @@ -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. @@ -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>) { + pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap>>) { self.document_metadata.layer_vector_data = new_layer_vector_data; } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 13c03f63f9..b0336b06c4 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -391,7 +391,9 @@ pub struct RenderMetadata { /// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage. pub text_frames: HashMap, pub clip_targets: HashSet, - pub vector_data: HashMap>, + // `RenderMetadata` only enters serialization via `TaggedValue::RenderOutput`, which also skips serde. + #[cfg_attr(feature = "serde", serde(skip))] + pub vector_data: HashMap>>, pub backgrounds: Vec, } @@ -1525,6 +1527,11 @@ impl Render for List { let mut accumulated_click_targets: HashMap>> = HashMap::new(); let mut accumulated_outlines: HashMap>> = HashMap::new(); + // Source geometry (not the click-target override) so editing tools work on letterforms. + if let Some(element_id) = caller_element_id { + metadata.vector_data.entry(element_id).or_insert_with(|| Arc::new(self.clone())); + } + for index in 0..self.len() { let Some(source) = self.element(index) else { continue }; let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); @@ -1554,10 +1561,6 @@ impl Render for List { extend_targets_from_vector(&mut outlines_unwrapped, source, item_relative_transform); accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new)); - // Source geometry (not the click-target override) so editing tools work on letterforms. - // Only item 0 is recorded since editing tools can only target a single item currently. - metadata.vector_data.entry(element_id).or_insert_with(|| Arc::new(source.clone())); - // Surface `editor:text_frame` for the Text tool's drag cage if let Some(&frame) = self.attribute::(ATTR_EDITOR_TEXT_FRAME, index) { metadata.text_frames.entry(element_id).or_insert(frame);