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.
This commit is contained in:
YohYamasaki
2026-05-20 16:09:39 +09:00
parent 1adeb4cedd
commit 4285243a5d
5 changed files with 48 additions and 19 deletions

View File

@@ -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<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,

View File

@@ -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<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 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::<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 {

View File

@@ -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<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,
}
@@ -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);

View File

@@ -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<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;
}
}

View File

@@ -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<NodeId, DAffine2>,
pub clip_targets: HashSet<NodeId>,
pub vector_data: HashMap<NodeId, Arc<Vector>>,
// `RenderMetadata` only enters serialization via `TaggedValue::RenderOutput`, which also skips serde.
#[cfg_attr(feature = "serde", serde(skip))]
pub vector_data: HashMap<NodeId, Arc<List<Vector>>>,
pub backgrounds: Vec<Background>,
}
@@ -1525,6 +1527,11 @@ impl Render for List<Vector> {
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
let mut accumulated_outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>> = 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<Vector> {
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::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index) {
metadata.text_frames.entry(element_id).or_insert(frame);