From f726e712bc1efa0292d05674d255b9373fde0a8c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 9 Sep 2026 22:44:18 +0000 Subject: [PATCH] Retire the stroke paint order input into fill and stroke node order The Stroke node no longer takes a paint order value: stroke-below is now the Stroke node sitting upstream of the Fill node, and the editor's paint order control rewires the two nodes through set_stroke_paint_order. Documents with the retired input migrate to the nine-input shape, their stored order replayed as that topology rewrite. The Stroke struct drops its paint_order field; renderers read stroke_below from the resolved appearance alone. Co-Authored-By: Claude Fable 5 --- .../data_panel/data_panel_message_handler.rs | 4 -- .../graph_operation_message.rs | 6 +- .../graph_operation_message_handler.rs | 11 +++- .../document/graph_operation/utility_types.rs | 66 ++++++++++++++++++- .../document/node_graph/node_properties.rs | 4 -- .../messages/portfolio/document_migration.rs | 33 ++++++++-- .../common_functionality/color_selector.rs | 8 ++- .../graph_modification_utils.rs | 19 +++++- .../common_functionality/stroke_options.rs | 5 +- .../libraries/rendering/src/render_ext.rs | 2 +- .../libraries/rendering/src/renderer.rs | 32 +++------ .../vector-types/src/vector/style.rs | 5 -- node-graph/nodes/gstd/src/render_cache.rs | 8 +-- node-graph/nodes/path-bool/src/lib.rs | 2 +- node-graph/nodes/vector/src/vector_nodes.rs | 48 +++++--------- 15 files changed, 164 insertions(+), 89 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index c9bda31717..007df4731f 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -430,10 +430,6 @@ impl TableItemLayout for Vector { TextLabel::new("Stroke Transform").narrow(true).widget_instance(), TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(), ]); - table_rows.push(vec![ - TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(), - TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_instance(), - ]); } let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::>().join(", "); diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 7d986bce1b..3a9f535681 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, PaintOrder, Stroke}; use graphene_std::vector::{GradientStops, PointId, VectorModificationType}; #[impl_message(Message, DocumentMessage, GraphOperation)] @@ -63,6 +63,10 @@ pub enum GraphOperationMessage { color: Option, stroke: Stroke, }, + StrokePaintOrderSet { + layer: LayerNodeIdentifier, + paint_order: PaintOrder, + }, TransformChange { layer: LayerNodeIdentifier, transform: DAffine2, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 4e64599e79..4da1e78a9b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput}; use graphene_std::list::List; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{Artboard, Color}; #[derive(ExtractField)] @@ -96,6 +96,14 @@ impl MessageHandler> for modify_inputs.stroke_set(color, stroke); } } + GraphOperationMessage::StrokePaintOrderSet { layer, paint_order } => { + if let Some(stroke_node_id) = crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_id(layer, network_interface) { + if super::utility_types::set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order) { + responses.add(PropertiesPanelMessage::Refresh); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + } + } GraphOperationMessage::TransformChange { layer, transform, @@ -792,7 +800,6 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont }, join_miter_limit: stroke.miterlimit().get() as f64, align: StrokeAlign::Center, - paint_order: PaintOrder::StrokeAbove, transform, }, ) diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 0230a30e9b..51827991ec 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientType, HasTransform, Stroke}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, HasTransform, PaintOrder, Stroke}; use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; @@ -732,8 +732,6 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false); - let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX); @@ -869,3 +867,65 @@ impl<'a> ModifyInputsContext<'a> { } } } + +/// The wires feeding off a node's primary output. +fn primary_output_consumers(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], node_id: NodeId) -> Vec { + network_interface + .outward_wires(network_path) + .and_then(|wires| wires.get(&OutputConnector::node(node_id, 0)).cloned()) + .unwrap_or_default() +} + +/// The fill wired directly to the stroke on either side, and whether the stroke currently paints above it. +/// The downstream node of the pair appends its cover last, so it paints on top. +pub fn adjacent_fill_of_stroke(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId) -> Option<(NodeId, bool)> { + let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER); + let is_fill = |network_interface: &NodeNetworkInterface, node_id: &NodeId| network_interface.reference(node_id, network_path) == Some(fill_reference.clone()); + + let stroke_primary_source = match network_interface.input_from_connector(&InputConnector::node(stroke_node_id, 0), network_path) { + Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id), + _ => None, + }; + if let Some(source) = stroke_primary_source.filter(|source| is_fill(network_interface, source)) { + return Some((source, true)); + } + let consumers = primary_output_consumers(network_interface, network_path, stroke_node_id); + let fill_consumer = consumers.iter().find_map(|connector| match connector { + InputConnector::Node { node_id, input_index: 0 } if is_fill(network_interface, node_id) => Some(*node_id), + _ => None, + }); + fill_consumer.map(|fill_node_id| (fill_node_id, false)) +} + +/// Swaps a chain's directly adjacent Stroke and Fill nodes when their order disagrees with the requested +/// paint order: both nodes append their cover, so the downstream one of the pair paints on top, following +/// the painter's algorithm. Without a fill wired directly to the stroke, nothing changes. +/// Returns whether the graph changed. +pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool { + let Some((fill_node_id, currently_above)) = adjacent_fill_of_stroke(network_interface, network_path, stroke_node_id) else { + return false; + }; + + if (paint_order == PaintOrder::StrokeAbove) == currently_above { + return false; + } + + // Swap the pair in place: the downstream node takes the upstream one's source, consumers of the + // downstream node move over to the upstream one, and the wire linking the pair reverses direction + let (upstream, downstream) = if currently_above { (fill_node_id, stroke_node_id) } else { (stroke_node_id, fill_node_id) }; + let Some(upstream_source) = network_interface.input_from_connector(&InputConnector::node(upstream, 0), network_path).cloned() else { + return false; + }; + let downstream_consumers = primary_output_consumers(network_interface, network_path, downstream); + + network_interface.set_input(&InputConnector::node(downstream, 0), upstream_source, network_path); + network_interface.set_input(&InputConnector::node(upstream, 0), NodeInput::node(downstream, 0), network_path); + for consumer in &downstream_consumers { + if matches!(consumer, InputConnector::Node { node_id, .. } if *node_id == upstream || *node_id == downstream) { + continue; + } + network_interface.set_input(consumer, NodeInput::node(upstream, 0), network_path); + } + + true +} diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 3eef8bf647..6857205b74 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2760,9 +2760,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - ParameterWidgetsInfo::new(node_id, MiterLimitInput::INDEX, true, context), NumberInput::default().min(0.).disabled(miter_limit_disabled), ); - let paint_order = enum_choice::() - .for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context)) - .property_row(); let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths); let dash_lengths = array_of_number_widget(ParameterWidgetsInfo::new(node_id, DashLengthsInput::INDEX, true, context), TextInput::default().centered(true)); let number_input = disabled_number_input; @@ -2775,7 +2772,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - cap, join, LayoutGroup::row(miter_limit), - paint_order, LayoutGroup::row(dash_lengths), LayoutGroup::row(dash_offset), ] diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 10c8c67f38..7758e47bd6 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1,6 +1,7 @@ // TODO: Eventually remove this document upgrade code // This file contains lots of hacky code for upgrading old documents to the new format +use crate::messages::portfolio::document::graph_operation::utility_types::set_stroke_paint_order; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; @@ -1745,13 +1746,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], inputs_count = 8; } - // Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) + // Upgrade Stroke node to reorder parameters and add "Align" (#2644), landing on the shape without the retired "Paint Order" input if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); - let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false); document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); @@ -1760,9 +1760,32 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 7), old_inputs[3].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[4].clone(), network_path); + + inputs_count = 9; + } + + // The Stroke node's "Paint Order" input was retired in favor of the relative order of the Fill and Stroke + // nodes in the chain, so the stored value becomes a topology rewrite that reorders the two nodes. + if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 10 { + let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + + // A wired paint order input cannot be evaluated statically, so it degrades to the default and leaves its source disconnected + let paint_order = match old_inputs.get(7).and_then(|input| input.as_value()) { + Some(&TaggedValue::PaintOrder(value)) => value, + _ => PaintOrder::StrokeAbove, + }; + + for (index, input) in old_inputs.iter().enumerate().take(7) { + document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); + } + document.network_interface.set_input(&InputConnector::node(*node_id, 7), old_inputs[8].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[9].clone(), network_path); + inputs_count = 9; + + set_stroke_paint_order(&mut document.network_interface, network_path, *node_id, paint_order); } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 0bd902e005..4472785fcb 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -182,12 +182,18 @@ impl DrawingToolState { cap: self.stroke_cap.unwrap_or_default(), join: self.stroke_join.unwrap_or_default(), join_miter_limit: self.miter_limit.unwrap_or(4.), - paint_order: self.paint_order.unwrap_or_default(), dash_lengths: self.effective_dash_lengths(), dash_offset: self.dash_offset.unwrap_or(0.), transform: glam::DAffine2::IDENTITY, }; responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); + // The paint order is the chain order of the Fill and Stroke nodes, so a below choice reorders the new pair + if self.paint_order.unwrap_or_default() == graphene_std::vector::style::PaintOrder::StrokeBelow { + responses.add(GraphOperationMessage::StrokePaintOrderSet { + layer, + paint_order: graphene_std::vector::style::PaintOrder::StrokeBelow, + }); + } } } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 119fca7c28..171f739040 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -565,9 +565,22 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe Some(TaggedValue::F64(value)) => *value, _ => 4., }; - let paint_order = match read(graphene_std::vector::stroke::PaintOrderInput::INDEX) { - Some(TaggedValue::PaintOrder(value)) => *value, - _ => PaintOrder::default(), + // The paint order is the chain order of the Fill and Stroke nodes: the downstream one of an adjacent pair paints on top + let paint_order = { + let stroke_node_id = get_stroke_id(layer, network_interface); + let primary_source = |node_id: Option| { + node_id.and_then(|node_id| match network_interface.input_from_connector(&InputConnector::node(node_id, 0), &[]) { + Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id), + _ => None, + }) + }; + if primary_source(stroke_node_id).is_some_and(|source| Some(source) == get_fill_id(layer, network_interface)) { + PaintOrder::StrokeAbove + } else if primary_source(get_fill_id(layer, network_interface)).is_some_and(|source| Some(source) == stroke_node_id) { + PaintOrder::StrokeBelow + } else { + PaintOrder::default() + } }; let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::INDEX) { Some(TaggedValue::F64Array(value)) => value.clone(), diff --git a/editor/src/messages/tool/common_functionality/stroke_options.rs b/editor/src/messages/tool/common_functionality/stroke_options.rs index a6b9e7744c..f6935d14b2 100644 --- a/editor/src/messages/tool/common_functionality/stroke_options.rs +++ b/editor/src/messages/tool/common_functionality/stroke_options.rs @@ -209,7 +209,10 @@ pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: & pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque) { drawing.paint_order = Some(order); - set_stroke_input_for_selected(document, graphene_std::vector::stroke::PaintOrderInput::INDEX, TaggedValue::PaintOrder(order), responses); + // The paint order is the chain order of the Fill and Stroke nodes, so applying it reorders the pair + for layer in document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface) { + responses.add(GraphOperationMessage::StrokePaintOrderSet { layer, paint_order: order }); + } } pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec, document: &DocumentMessageHandler, responses: &mut VecDeque) { diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index d5ae2d346e..714c4014b3 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -213,7 +213,7 @@ impl RenderExt for Stroke { let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join); let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit); let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align); - let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow); + let paint_order = render_params.stroke_below.then_some(PaintOrder::StrokeBelow); // Render the needed stroke attributes let mut attributes = String::new(); diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 3d02a85bcf..cc75d98283 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -31,7 +31,7 @@ use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; -use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; +use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::{ATTR_FILL, Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use num_traits::Zero; @@ -302,7 +302,7 @@ pub struct RenderParams { /// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks pub alignment_parent_transform: Option, pub aligned_strokes: bool, - pub override_paint_order: bool, + pub stroke_below: bool, /// Are we rendering for a pattern content pub inside_pattern: bool, pub artboard_background: Option, @@ -1340,11 +1340,7 @@ fn render_vector_svg>(source: &S, inherited_appe // The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); - let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); - // The paint order rides the coverage list's row order - if let Some(stroke) = &mut resolved.stroke { - stroke.paint_order = if resolved.stroke_below { PaintOrder::StrokeBelow } else { PaintOrder::StrokeAbove }; - } + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); let element_stroke = resolved.stroke.as_ref(); // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform @@ -1387,7 +1383,8 @@ fn render_vector_svg>(source: &S, inherited_appe let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip); let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; - let wants_stroke_below = element_stroke.map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); + // The paint order rides the coverage list's row order + let wants_stroke_below = resolved.stroke_below; let override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let use_face_fill = vector.use_face_fill(); @@ -1474,7 +1471,7 @@ fn render_vector_svg>(source: &S, inherited_appe let mut render_params = render_params.clone(); render_params.aligned_strokes = can_draw_aligned_stroke; - render_params.override_paint_order = override_paint_order; + render_params.stroke_below = override_paint_order || wants_stroke_below; let stroke_shape_attribute = element_stroke .map(|stroke| { @@ -1559,8 +1556,6 @@ fn render_vector_vello>( render_params: &RenderParams, ) { for index in 0..source.lane_count() { - use graphic_types::vector_types::vector; - let Some(element) = source.element(index) else { continue }; let item_transform: DAffine2 = source.attr::(index); let blend_mode_attr: BlendMode = source.attr::(index); @@ -1570,15 +1565,7 @@ fn render_vector_vello>( // The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); - let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); - // The paint order rides the coverage list's row order - if let Some(stroke) = &mut resolved.stroke { - stroke.paint_order = if resolved.stroke_below { - vector::style::PaintOrder::StrokeBelow - } else { - vector::style::PaintOrder::StrokeAbove - }; - } + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); let has_real_stroke = resolved.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); @@ -1641,7 +1628,8 @@ fn render_vector_vello>( } let use_layer = can_draw_aligned_stroke; - let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); + // The paint order rides the coverage list's row order + let wants_stroke_below = resolved.stroke_below; let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| { let Some(fill_graphic) = fill_graphic_list else { return }; @@ -1827,7 +1815,7 @@ fn render_vector_vello>( Stroke, } - let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) { + let order = match stroke.is_some() && wants_stroke_below { true => [Op::Stroke, Op::Fill], false => [Op::Fill, Op::Stroke], // Default }; diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index bcb4828986..1db95e6e3c 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -230,8 +230,6 @@ pub struct Stroke { pub align: StrokeAlign, #[cfg_attr(feature = "serde", serde(default = "daffine2_identity"))] pub transform: DAffine2, - #[cfg_attr(feature = "serde", serde(default))] - pub paint_order: PaintOrder, } impl Stroke { @@ -245,7 +243,6 @@ impl Stroke { join_miter_limit: 4., align: StrokeAlign::Center, transform: DAffine2::IDENTITY, - paint_order: PaintOrder::StrokeAbove, } } @@ -282,7 +279,6 @@ impl Stroke { let skew = DAffine2::from_cols_array(&[1., 0., lerp(s_skew, t_skew), 1., 0., 0.]); trs * skew }, - paint_order: if time < 0.5 { self.paint_order } else { other.paint_order }, } } @@ -410,7 +406,6 @@ impl Default for Stroke { join_miter_limit: 4., align: StrokeAlign::Center, transform: DAffine2::IDENTITY, - paint_order: PaintOrder::default(), } } } diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 304c570e98..9e1e6a4586 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -43,7 +43,7 @@ pub struct CacheKey { pub for_mask: bool, pub thumbnail: bool, pub aligned_strokes: bool, - pub override_paint_order: bool, + pub stroke_below: bool, pub animation_time_ms: i64, pub real_time_ms: i64, pub pointer: [u8; 16], @@ -60,7 +60,7 @@ impl CacheKey { for_mask: bool, thumbnail: bool, aligned_strokes: bool, - override_paint_order: bool, + stroke_below: bool, animation_time: f64, real_time: f64, pointer: Option, @@ -85,7 +85,7 @@ impl CacheKey { for_mask, thumbnail, aligned_strokes, - override_paint_order, + stroke_below, animation_time_ms: (animation_time * 1000.).round() as i64, real_time_ms: (real_time * 1000.).round() as i64, pointer: pointer_bytes, @@ -360,7 +360,7 @@ pub fn render_output_cache( render_params.for_mask, render_params.thumbnail, render_params.aligned_strokes, - render_params.override_paint_order, + render_params.stroke_below, ctx.try_animation_time().unwrap_or(0.), ctx.try_real_time().unwrap_or(0.), ctx.try_pointer_position(), diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3a17f26f83..83322ad3a9 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -3,7 +3,7 @@ use core_types::list::{Item, List}; use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; use glam::{DAffine2, DVec2}; -use graphic_types::appearance::{Appearance, CoverPlacement, Coverage}; +use graphic_types::appearance::{Appearance, Coverage}; use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms, is_paint_present, set_paint_attribute, set_paint_attribute_at}; use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers, Fill, Stroke}; use graphic_types::raster_types::{CPU, GPU, Raster}; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index b881d6bfb0..17a03d7ab6 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -38,7 +38,7 @@ use vector_types::vector::misc::{ CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, }; -use vector_types::vector::style::{GradientStops, HasTransform, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use vector_types::vector::style::{GradientStops, HasTransform, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; use vector_types::{GradientSpreadMethod, GradientType}; @@ -323,15 +323,6 @@ fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List(arena: &'e core_types::arena::Arena, attributes: &ItemAttributeValues) -> Result, Interrupt> { - attributes - .get::(graphic_types::ATTR_APPEARANCE) - .cloned() - .map(|appearance| park_appearance(arena, appearance)) - .transpose() -} - /// Keyed, as [`park_paint`] is, so a group-free appearance's promote moves this header. fn park_appearance<'e>(arena: &'e core_types::arena::Arena, appearance: Appearance) -> Result<&'e Appearance, Interrupt> { let (parked, _) = arena.alloc_sized_keyed(appearance, 0).ok_or(GraphError { @@ -464,8 +455,6 @@ fn stroke<'e>( /// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio. #[default(4.)] miter_limit: f64, - /// The order to paint the stroke on top of the fill, or the fill on top of the stroke. - paint_order: PaintOrder, /// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed. dash_lengths: IList, /// The phase offset distance from the starting point of the dash pattern. @@ -482,7 +471,6 @@ fn stroke<'e>( join_miter_limit: miter_limit, align, transform: DAffine2::IDENTITY, - paint_order, }; // The coverage records the stroke's authoring space, so the item transform is composed in, translation @@ -496,12 +484,9 @@ fn stroke<'e>( element.stroke = Some(stroke); let paint = paint_table(paint); - // The coverage order carries the paint order: a below stroke lands before the fill in the list - let placement = match paint_order { - PaintOrder::StrokeAbove => CoverPlacement::Above, - PaintOrder::StrokeBelow => CoverPlacement::Below, - }; - let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, placement); + // The paint order is the coverage row order: appending above follows the painter's algorithm, and a + // below stroke is expressed by the chain running the stroke node before the fill + let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above); let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; Ok((element, Attr(*content_transform), Attr(Some(parked)), Attr(Some(parked_appearance)))) @@ -539,7 +524,6 @@ fn stroke_graphic_leveled<'e>( cap: StrokeCap, join: StrokeJoin, #[default(4.)] miter_limit: f64, - paint_order: PaintOrder, dash_lengths: IList, #[unit(" px")] dash_offset: f64, ) -> Result<(Graphic<'static>, Attr, Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>), Interrupt> { @@ -553,7 +537,6 @@ fn stroke_graphic_leveled<'e>( join_miter_limit: miter_limit, align, transform: DAffine2::IDENTITY, - paint_order, }; // The coverage records the stroke's authoring space at the lane, composing the lane transform as in `stroke` above. @@ -568,12 +551,9 @@ fn stroke_graphic_leveled<'e>( }); let paint = paint_table(paint); - // The coverage order carries the paint order: a below stroke lands before the fill in the list - let placement = match paint_order { - PaintOrder::StrokeAbove => CoverPlacement::Above, - PaintOrder::StrokeBelow => CoverPlacement::Below, - }; - let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, placement); + // The paint order is the coverage row order: appending above follows the painter's algorithm, and a + // below stroke is expressed by the chain running the stroke node before the fill + let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above); let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; Ok((element, Attr(*content_transform), Attr(Some(parked)), Attr(Some(parked_appearance)))) @@ -1485,7 +1465,11 @@ fn solidify_rows(flattened: List) -> List { let dash_offset = stroke.dash_offset; let dash_pattern = stroke.dash_lengths; let miter_limit = stroke.join_miter_limit; - let paint_order = stroke.paint_order; + // The paint order rides the row's coverage order + let stroke_below = attributes + .get::(graphic_types::ATTR_APPEARANCE) + .map(Appearance::fill_and_stroke) + .is_some_and(|resolved| resolved.stroke_below); let stroke_style = kurbo::Stroke::new(stroke.weight) .with_caps(cap) @@ -1541,10 +1525,10 @@ fn solidify_rows(flattened: List) -> List { let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); - // Ordering based on the paint order. The first item in the `List` is rendered below the second. - match paint_order { - PaintOrder::StrokeAbove => fill_row.into_iter().chain(std::iter::once(stroke_row)).collect::>(), - PaintOrder::StrokeBelow => std::iter::once(stroke_row).chain(fill_row).collect::>(), + // Ordering based on the coverage order. The first item in the `List` is rendered below the second. + match stroke_below { + false => fill_row.into_iter().chain(std::iter::once(stroke_row)).collect::>(), + true => std::iter::once(stroke_row).chain(fill_row).collect::>(), } }) .collect();