From 097ce929c5a41de6c3c561933334199569f3bb63 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 11 Aug 2026 00:25:54 -0700 Subject: [PATCH 1/5] Stop chain node insertion from stranding a node on layers without a chain --- .../portfolio/document/graph_operation/utility_types.rs | 3 +++ 1 file changed, 3 insertions(+) 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 6fbce96bc8..1a4bd00da8 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -397,6 +397,9 @@ impl<'a> ModifyInputsContext<'a> { return None; }; + // Without a secondary input there is no chain to hold the node, so inserting it would strand it at the graph origin + self.network_interface.input_from_connector(&InputConnector::layer_secondary_input(output_layer.to_node()), &[])?; + // If inserting a 'Path' node, insert a 'Combine Paths' node if the type is `Graphic`. // TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item. if node_definition.identifier == "Path" { From 38290e20c017ebb9786a52bdf4b48fc824cf6c63 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 11 Aug 2026 00:26:04 -0700 Subject: [PATCH 2/5] Gate the Layers panel blending controls on whether the layer can host them --- .../document/document_message_handler.rs | 9 +++- .../document/graph_operation/utility_types.rs | 20 +++++-- .../node_graph/node_graph_message_handler.rs | 2 +- .../network_interface/resolved_types.rs | 52 +++++++++++++++---- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index bb1c0113d7..5908d70c64 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -3409,6 +3409,11 @@ impl DocumentMessageHandler { let selected_nodes = self.network_interface.selected_nodes(); let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface); + // A layer whose chain cannot carry blending nodes has nowhere to put the value, so it disqualifies the whole selection + let all_layers_support_blending = selected_nodes + .selected_layers_except_artboards(&self.network_interface) + .all(|layer| self.network_interface.layer_hosts_blending_nodes(&layer.to_node(), &[])); + // Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest. let mut blending_options = selected_layers_except_artboards.map(|layer| { ( @@ -3420,8 +3425,8 @@ impl DocumentMessageHandler { let first_blending_options = blending_options.next(); let result_blending_options = blending_options; - // If there are no selected layers, disable the opacity and blend mode widgets. - let disabled = first_blending_options.is_none(); + // If there are no selected layers, or any of them cannot host the nodes, disable the opacity and blend mode widgets. + let disabled = first_blending_options.is_none() || !all_layers_support_blending; // Amongst the selected layers, check if the opacities and blend modes are identical across all layers. // The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not. 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 1a4bd00da8..3272746143 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -347,6 +347,16 @@ impl<'a> ModifyInputsContext<'a> { self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent) } + /// The same as [`Self::existing_proto_node_id`], but yielding `None` on layers whose chain cannot host the node. + fn existing_chain_hosted_node_id(&mut self, reference: ProtoNodeIdentifier, create_if_nonexistent: bool) -> Option { + let output_layer = self.get_output_layer()?; + if !self.network_interface.layer_chain_hosts_node(&output_layer.to_node(), &[], &reference) { + return None; + } + + self.existing_proto_node_id(reference, create_if_nonexistent) + } + /// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist. fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option { // Start from the layer node or export @@ -480,7 +490,7 @@ impl<'a> ModifyInputsContext<'a> { } pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { - let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else { + let Some(blend_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else { return; }; let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput); @@ -488,7 +498,7 @@ impl<'a> ModifyInputsContext<'a> { } pub fn opacity_set(&mut self, opacity: f64) { - let Some(opacity_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else { + let Some(opacity_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else { return; }; // Enable the `has_opacity` checkbox so the value is applied @@ -507,9 +517,9 @@ impl<'a> ModifyInputsContext<'a> { pub fn opacity_fill_set(&mut self, fill: f64) { // Reuse an existing Opacity node to avoid a redundant chain walk on slider drags let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER; - let existing = self.existing_proto_node_id(identifier.clone(), false); + let existing = self.existing_chain_hosted_node_id(identifier.clone(), false); let existed = existing.is_some(); - let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else { + let Some(opacity_node_id) = existing.or_else(|| self.existing_chain_hosted_node_id(identifier, true)) else { return; }; // Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently @@ -823,7 +833,7 @@ impl<'a> ModifyInputsContext<'a> { pub fn clip_mode_toggle(&mut self, clip_mode: Option) { let clip = !clip_mode.unwrap_or(false); - let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else { + let Some(clip_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else { return; }; let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 7efd6efa72..13632c96a9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -2852,7 +2852,7 @@ impl NodeGraphMessageHandler { })) ); - let clippable = layer.can_be_clipped(network_interface.document_metadata()); + let clippable = layer.can_be_clipped(network_interface.document_metadata()) && network_interface.layer_hosts_blending_nodes(&node_id, &[]); let data = LayerPanelEntry { id: node_id, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs index 4127baee83..a11f048b51 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput}; use graph_craft::proto::{GraphErrorType, GraphErrors}; -use graph_craft::{Type, concrete}; +use graph_craft::{ProtoNodeIdentifier, Type, concrete}; use graphene_std::uuid::NodeId; use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::node_registry::NODE_REGISTRY; @@ -63,15 +63,14 @@ impl TypeSource { self.compiled_nested_type().is_some_and(|ty| matches!(ty, Type::List(_)) || ty.bundle_element_name().is_some()) } - /// The element type's identifier name with any rank-0 `Item` or rank-1 `List` wrapper peeled, so semantic type checks can be rank-agnostic. + /// The element type with any rank-0 `Item` or rank-1 `List` wrapper peeled, so semantic type checks can be rank-agnostic. + pub fn compiled_element_type(&self) -> Option<&Type> { + Some(element_of(self.compiled_nested_type()?)) + } + + /// The identifier name of [`Self::compiled_element_type`]. pub fn compiled_element_name(&self) -> Option { - let nested_type = self.compiled_nested_type()?; - // A rank-0 `Item` or rank-1 `List` peels to its element; a bare value reports itself - let element = match nested_type { - Type::Item(element) | Type::List(element) => element.as_ref(), - other => other, - }; - Some(element.identifier_name()) + Some(self.compiled_element_type()?.identifier_name()) } pub fn compiled_nested_type(&self) -> Option<&Type> { @@ -110,6 +109,14 @@ impl TypeSource { } } +/// Peels any `Fn`/`Future` wrapper, then any rank-0 `Item` or rank-1 `List` wrapper, down to the element type. +fn element_of(ty: &Type) -> &Type { + match ty.nested_type() { + Type::Item(element) | Type::List(element) => element.as_ref(), + other => other, + } +} + impl NodeNetworkInterface { fn input_has_error(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> bool { match input_connector { @@ -169,6 +176,33 @@ impl NodeNetworkInterface { } } + /// Whether the given node has a registered implementation accepting the layer chain's element type as its content input. + /// A chain awaiting compilation has no resolved type yet, so only a known-wrong type or a type error locks the layer out. + pub fn layer_chain_hosts_node(&self, node_id: &NodeId, network_path: &[NodeId], node: &ProtoNodeIdentifier) -> bool { + let secondary_input = InputConnector::layer_secondary_input(*node_id); + if !self.input_from_connector(&secondary_input, network_path).is_some_and(|input| input.is_exposed()) { + return false; + } + + let chain_type = self.input_type(&secondary_input, network_path); + match chain_type.compiled_element_type() { + Some(element) => { + let Some(implementations) = NODE_REGISTRY.get(node) else { + log::error!("Proto node {node:?} not found in the node registry, in layer_chain_hosts_node"); + return false; + }; + implementations.keys().any(|node_io| node_io.inputs.first().is_some_and(|content| element_of(content) == element)) + } + None => !matches!(chain_type, TypeSource::Invalid), + } + } + + /// Whether the blending nodes (blend mode, opacity, clipping mask) can be spliced into this layer's chain. + pub fn layer_hosts_blending_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + // Blend Mode stands in for the trio since they share one implementations list + self.layer_chain_hosts_node(node_id, network_path, &graphene_std::blending_nodes::blend_mode::IDENTIFIER) + } + /// Get the [`TypeSource`] for any InputConnector. /// If the input is not compiled, then an Unknown or default from the definition is returned. pub fn input_type(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource { From 07f5a7cdbbb5764a613ca8dfd5c4df73e39bd8fe Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 11 Aug 2026 00:42:47 -0700 Subject: [PATCH 3/5] Gate the fill and stroke setters on whether the layer can host paint nodes --- .../portfolio/document/graph_operation/utility_types.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 3272746143..d792346fe0 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -431,7 +431,7 @@ impl<'a> ModifyInputsContext<'a> { } pub fn fill_color_set(&mut self, color: Option) { - let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { + let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput); @@ -446,7 +446,7 @@ impl<'a> ModifyInputsContext<'a> { } pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) { - let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { + let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput); @@ -841,7 +841,7 @@ impl<'a> ModifyInputsContext<'a> { } pub fn stroke_set(&mut self, color: Option, stroke: Stroke) { - let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else { + let Some(stroke_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else { return; }; From 57f07c73639b33b13609e5d43f265a4eb6afa7c5 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 11 Aug 2026 01:19:13 -0700 Subject: [PATCH 4/5] Make the tool options fill and stroke controls treat layers that cannot take paint as unselected --- .../network_interface/resolved_types.rs | 6 ++++ .../common_functionality/color_selector.rs | 35 ++++++++----------- .../graph_modification_utils.rs | 33 +++++++++-------- .../tool/tool_messages/select_tool.rs | 12 ++++--- .../messages/tool/tool_messages/shape_tool.rs | 10 +++--- 5 files changed, 51 insertions(+), 45 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs index a11f048b51..e9fb310e9a 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -203,6 +203,12 @@ impl NodeNetworkInterface { self.layer_chain_hosts_node(node_id, network_path, &graphene_std::blending_nodes::blend_mode::IDENTIFIER) } + /// Whether the Fill and Stroke nodes can be spliced into this layer's chain. + pub fn layer_hosts_paint_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + // Fill stands in for both since they share one implementations list + self.layer_chain_hosts_node(node_id, network_path, &graphene_std::vector_nodes::fill::IDENTIFIER) + } + /// Get the [`TypeSource`] for any InputConnector. /// If the input is not compiled, then an Unknown or default from the definition is returned. pub fn input_type(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource { diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 2a9636300c..110a78badb 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -304,10 +304,8 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled: /// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates /// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is. fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool { - let strokes: Vec<_> = document - .network_interface - .selected_nodes() - .selected_layers_except_artboards(&document.network_interface) + let strokes: Vec<_> = graph_modification_utils::paintable_selected_layers(document) + .into_iter() .filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface)) .collect(); if strokes.is_empty() { @@ -391,14 +389,9 @@ pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, f } } -/// True if at least one (non-artboard) layer is currently selected. -pub fn has_selection(document: &DocumentMessageHandler) -> bool { - document - .network_interface - .selected_nodes() - .selected_layers_except_artboards(&document.network_interface) - .next() - .is_some() +/// True if at least one selected layer can take paint, making the swatches edit the selection instead of the tool's own colors. +pub fn has_paintable_selection(document: &DocumentMessageHandler) -> bool { + !graph_modification_utils::paintable_selected_layers(document).is_empty() } /// Applies a user-picked fill (gradient or solid). With a selection, writes to the layers; with none, pushes a solid to the swap-routed working color slot. @@ -411,7 +404,7 @@ pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: Fill fill.fill_choice = Some(fill_choice.clone()); fill.enabled = Some(true); fill.tracks_working_color = false; - if has_selection(document) { + if has_paintable_selection(document) { if document.network_interface.transaction_status() == TransactionStatus::Finished { responses.add(DocumentMessage::StartTransaction); } @@ -426,7 +419,7 @@ pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option) { fill.enabled = Some(enabled); - if has_selection(document) { + if has_paintable_selection(document) { responses.add(DocumentMessage::AddTransaction); } if enabled { @@ -471,7 +464,7 @@ pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, worki /// Toggles the stroke checkbox: mirrors [`apply_fill_enabled`]. pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, global: &DocumentToolData, document: &DocumentMessageHandler, responses: &mut VecDeque) { drawing.stroke.enabled = Some(enabled); - if has_selection(document) { + if has_paintable_selection(document) { responses.add(DocumentMessage::AddTransaction); } if enabled { @@ -493,7 +486,7 @@ pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, globa /// Applies a user-edited stroke weight to the selection, also persisting it as the no-selection default. pub fn apply_line_weight(drawing: &mut DrawingToolState, line_weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque) { drawing.line_weight = Some(line_weight); - if !has_selection(document) { + if !has_paintable_selection(document) { drawing.default_line_weight = line_weight; } graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, document, responses); @@ -507,7 +500,7 @@ pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToo /// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`]. pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) { - if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) { + if slot.fill_choice.is_some() && (!has_paintable_selection(document) || slot.tracks_working_color) { slot.fill_choice = Some(solid(working_color)); } } @@ -524,7 +517,7 @@ pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &Doc pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentMessageHandler, responses: &mut VecDeque) { drawing.colors_swapped = !drawing.colors_swapped; - if has_selection(document) { + if has_paintable_selection(document) { responses.add(DocumentMessage::AddTransaction); } @@ -539,7 +532,7 @@ pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentM drawing.fill.tracks_working_color = new_fill_tracks; drawing.stroke.tracks_working_color = new_stroke_tracks; - if has_selection(document) { + if has_paintable_selection(document) { // Apply to layers only when we have a concrete value (`None` means mixed, no single value to broadcast). if drawing.fill.is_active() && let Some(choice) = new_fill @@ -579,7 +572,7 @@ pub enum WeightSyncOutcome { /// Inspects the selection and returns how the weight widget should update. pub fn compute_weight_sync(document: &DocumentMessageHandler) -> WeightSyncOutcome { - let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); + let layers = graph_modification_utils::paintable_selected_layers(document); if layers.is_empty() { return WeightSyncOutcome::NoSelection; 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 cd9cff31d0..02f2a8acd4 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -736,8 +736,7 @@ pub fn first_selected_stroke_weight(document: &DocumentMessageHandler) -> Option /// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new /// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes. pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque) { - let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); - for layer in layers { + for layer in paintable_selected_layers(document) { if let Some(node_id) = get_stroke_id(layer, &document.network_interface) { responses.add(NodeGraphMessage::SetInputValue { node_id, @@ -817,12 +816,21 @@ pub struct SelectedStrokeState { pub optional_color: Option>, } +/// The selected layers (artboards excluded) whose chains can host Fill and Stroke nodes. +/// The tool options bar's paint widgets treat these as the whole selection, so other layers act as if unselected. +pub fn paintable_selected_layers(document: &DocumentMessageHandler) -> Vec { + let selected_nodes = document.network_interface.selected_nodes(); + selected_nodes + .selected_layers_except_artboards(&document.network_interface) + .filter(|layer| document.network_interface.layer_hosts_paint_nodes(&layer.to_node(), &[])) + .collect() +} + /// Reads the fill state across all selected non-artboard layers, including whether their enabled states or colors differ. /// "Enabled" tracks node attachment: a layer counts as enabled whenever a Fill node is attached, even when that fill's value is the no-paint choice. -/// Unticked means there is no Fill node. Returns `None` only when no layer is selected. +/// Unticked means there is no Fill node. Returns `None` only when no paintable layer is selected. pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option { - let selected_nodes = document.network_interface.selected_nodes(); - let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| { + let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| { let Some(fill_node_id) = get_fill_id(layer, &document.network_interface) else { return (false, FillChoice::None); }; @@ -873,8 +881,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option Option { - let selected_nodes = document.network_interface.selected_nodes(); - let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| { + let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| { if get_stroke_id(layer, &document.network_interface).is_none() { return (false, None); } @@ -911,8 +918,7 @@ pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option