diff --git a/document-legacy/src/document_metadata.rs b/document-legacy/src/document_metadata.rs index dbb1d339..53bfe07d 100644 --- a/document-legacy/src/document_metadata.rs +++ b/document-legacy/src/document_metadata.rs @@ -41,6 +41,14 @@ impl DocumentMetadata { self.all_layers() } + pub fn selected_layers_contains(&self, layer: LayerNodeIdentifier) -> bool { + self.selected_layers().any(|selected| selected == layer) + } + + pub fn selected_visible_layers(&self) -> impl Iterator + '_ { + self.all_layers() + } + /// Access the [`NodeRelations`] of a layer fn get_relations(&self, node_identifier: LayerNodeIdentifier) -> Option<&NodeRelations> { self.structure.get(&node_identifier) @@ -90,12 +98,27 @@ impl DocumentMetadata { .filter_map(|layer| self.click_targets.get(&layer).map(|targets| (layer, targets))) .find(|(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.transform_from_document(*layer)))) } + + /// Get the bounding box of the click target of the specified layer in the specified transform space + pub fn bounding_box(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> { + self.click_targets + .get(&layer)? + .iter() + .filter_map(|click_target| click_target.subpath.bounding_box_with_transform(transform)) + .reduce(Quad::combine_bounds) + } } /// Id of a layer node #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct LayerNodeIdentifier(NonZeroU64); +impl Default for LayerNodeIdentifier { + fn default() -> Self { + Self::ROOT + } +} + impl LayerNodeIdentifier { const ROOT: Self = LayerNodeIdentifier::new_unchecked(0); @@ -283,6 +306,10 @@ impl LayerNodeIdentifier { document_metadata.structure.remove(&node); } } + + pub fn exists(&self, document_metadata: &DocumentMetadata) -> bool { + document_metadata.get_relations(*self).is_some() + } } impl From for LayerNodeIdentifier { 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 1ee0563c..8a05a9f0 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -2,9 +2,10 @@ use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::prelude::*; use bezier_rs::{ManipulatorGroup, Subpath}; -use document_legacy::{document_metadata::LayerNodeIdentifier, LayerId, Operation}; +use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier, LayerId, Operation}; use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork}; use graphene_core::uuid::ManipulatorGroupId; +use graphene_core::vector::style::{FillType, Gradient}; use glam::DAffine2; use std::collections::VecDeque; @@ -17,6 +18,7 @@ pub fn new_vector_layer(subpaths: Vec>, layer_path: }); } +/// Create a legacy node graph frame TODO: remove pub fn new_custom_layer(network: NodeNetwork, layer_path: Vec, responses: &mut VecDeque) { responses.add(DocumentMessage::DeselectAllLayers); responses.add(Operation::AddFrame { @@ -28,6 +30,7 @@ pub fn new_custom_layer(network: NodeNetwork, layer_path: Vec, response responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path }); } +/// Batch set all of the manipulator groups to mirror on a specific layer pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup], layer_path: &[u64], mirror_angle: bool, responses: &mut VecDeque) { for manipulator_group in manipulator_groups { responses.add(GraphOperationMessage::Vector { @@ -40,6 +43,64 @@ pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup Option<&Vec>> { + if let TaggedValue::Subpaths(subpaths) = NodeGraphLayer::new(layer, document)?.find_input("Shape", 0)? { + Some(subpaths) + } else { + None + } +} + +/// Get the currently mirrored handles for a particular layer from the shape node +pub fn get_mirror_handles(layer: LayerNodeIdentifier, document: &Document) -> Option<&Vec> { + if let TaggedValue::ManipulatorGroupIds(mirror_handles) = NodeGraphLayer::new(layer, document)?.find_input("Shape", 1)? { + Some(mirror_handles) + } else { + None + } +} + +/// Get the current gradient of a layer from the closest fill node +pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option { + let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?; + let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else { + return None; + }; + let TaggedValue::GradientType(gradient_type) = inputs.get(3)?.as_value()? else { + return None; + }; + let TaggedValue::DVec2(start) = inputs.get(4)?.as_value()? else { + return None; + }; + let TaggedValue::DVec2(end) = inputs.get(5)?.as_value()? else { + return None; + }; + let TaggedValue::DAffine2(transform) = inputs.get(6)?.as_value()? else { + return None; + }; + let TaggedValue::GradientPositions(positions) = inputs.get(7)?.as_value()? else { + return None; + }; + Some(Gradient { + start: start.clone(), + end: end.clone(), + transform: transform.clone(), + positions: positions.clone(), + gradient_type: gradient_type.clone(), + }) +} + +/// Convert subpaths to an iterator of manipulator groups +pub fn get_manipulator_groups(subpaths: &[Subpath]) -> impl Iterator> + DoubleEndedIterator { + subpaths.iter().flat_map(|subpath| subpath.manipulator_groups()) +} + +/// Find a manipulator group with a specific id from several subpaths +pub fn get_manipulator_from_id(subpaths: &[Subpath], id: ManipulatorGroupId) -> Option<&bezier_rs::ManipulatorGroup> { + subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id)) +} + /// An immutable reference to a layer within the document node graph for easy access. pub struct NodeGraphLayer<'a> { node_graph: &'a NodeNetwork, @@ -84,18 +145,18 @@ impl<'a> NodeGraphLayer<'a> { self.node_graph.primary_flow_from_opt(Some(self.layer_node)) } - /// Find a specific input of a node within the layer's primary flow - pub fn find_input(&self, node_name: &str, index: usize) -> Option<&'a TaggedValue> { + /// Find all of the inputs of a specific node within the layer's primary flow + pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec> { for (node, _id) in self.primary_layer_flow() { if node.name == node_name { - let subpaths_input = node.inputs.get(index)?; - let NodeInput::Value { tagged_value, .. } = subpaths_input else { - continue; - }; - - return Some(tagged_value); + return Some(&node.inputs); } } None } + + /// Find a specific input of a node within the layer's primary flow + pub fn find_input(&self, node_name: &str, index: usize) -> Option<&'a TaggedValue> { + self.find_node_inputs(node_name)?.get(index)?.as_value() + } } diff --git a/editor/src/messages/tool/common_functionality/overlay_renderer.rs b/editor/src/messages/tool/common_functionality/overlay_renderer.rs index a198553d..1775411d 100644 --- a/editor/src/messages/tool/common_functionality/overlay_renderer.rs +++ b/editor/src/messages/tool/common_functionality/overlay_renderer.rs @@ -3,7 +3,7 @@ use crate::application::generate_uuid; use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS; use crate::consts::{COLOR_ACCENT, HIDE_HANDLE_DISTANCE, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT}; use crate::messages::prelude::*; -use crate::messages::tool::tool_messages::pen_tool::{get_manipulator_groups, get_subpaths}; +use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths}; use bezier_rs::ManipulatorGroup; use document_legacy::document::Document; diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 200424b2..f304694c 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,7 +1,7 @@ use crate::consts::DRAG_THRESHOLD; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::prelude::*; -use crate::messages::tool::tool_messages::pen_tool::{get_manipulator_from_id, get_manipulator_groups, get_mirror_handles, get_subpaths}; +use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_manipulator_groups, get_mirror_handles, get_subpaths}; use bezier_rs::{Bezier, ManipulatorGroup, TValue}; use document_legacy::document::Document; diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 1b127b47..9e955885 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,11 +1,10 @@ use super::tool_prelude::*; use crate::application::generate_uuid; -use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD}; +use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient; use crate::messages::tool::common_functionality::snapping::SnapManager; -use document_legacy::intersection::Quad; -use document_legacy::layers::layer_info::Layer; -use document_legacy::layers::layer_layer::CachedOutputData; +use document_legacy::document_metadata::LayerNodeIdentifier; use document_legacy::layers::style::{Fill, Gradient, GradientType, PathStyle, RenderData, Stroke}; use document_legacy::LayerId; use document_legacy::Operation; @@ -115,12 +114,12 @@ enum GradientToolFsmState { Drawing, } -/// Computes the transform from gradient space to layer space (where gradient space is 0..1 in layer space) -fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, render_data: &RenderData) -> DAffine2 { - let bounds = layer.aabb_for_transform(DAffine2::IDENTITY, render_data).unwrap(); +/// Computes the transform from gradient space to viewport space (where gradient space is 0..1) +fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { + let bounds = document.document_legacy.metadata.bounding_box(layer, DAffine2::IDENTITY).unwrap(); let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - let multiplied = document.document_legacy.multiply_transforms(path).unwrap(); + let multiplied = document.document_legacy.metadata.transform_from_viewport(layer); multiplied * bound_transform } @@ -131,7 +130,7 @@ pub struct GradientOverlay { pub handles: [Vec; 2], pub line: Vec, pub steps: Vec>, - path: Vec, + layer: LayerNodeIdentifier, transform: DAffine2, gradient: Gradient, } @@ -174,17 +173,9 @@ impl GradientOverlay { path } - pub fn new( - fill: &Gradient, - dragging: Option, - path: &[LayerId], - layer: &Layer, - document: &DocumentMessageHandler, - responses: &mut VecDeque, - render_data: &RenderData, - ) -> Self { - let transform = gradient_space_transform(path, layer, document, render_data); - let Gradient { start, end, positions, .. } = fill; + pub fn new(gradient: Gradient, dragging: Option, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque) -> Self { + let transform = gradient_space_transform(layer, document); + let Gradient { start, end, positions, .. } = &gradient; let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)]; let line = Self::generate_overlay_line(start, end, responses); @@ -197,14 +188,11 @@ impl GradientOverlay { let create_step = |(index, pos)| Self::generate_overlay_handle(start.lerp(end, pos), responses, dragging == Some(GradientDragTarget::Step(index))); let steps = positions.iter().map(|(pos, _)| *pos).enumerate().filter(not_at_end).map(create_step).collect(); - let path = path.to_vec(); - let gradient = fill.clone(); - Self { handles, steps, line, - path, + layer, transform, gradient, } @@ -240,17 +228,17 @@ pub enum GradientDragTarget { /// Contains information about the selected gradient handle #[derive(Clone, Debug, Default)] struct SelectedGradient { - path: Vec, + layer: LayerNodeIdentifier, transform: DAffine2, gradient: Gradient, dragging: GradientDragTarget, } impl SelectedGradient { - pub fn new(gradient: Gradient, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, render_data: &RenderData) -> Self { - let transform = gradient_space_transform(path, layer, document, render_data); + pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self { + let transform = gradient_space_transform(layer, document); Self { - path: path.to_vec(), + layer, transform, gradient, dragging: GradientDragTarget::End, @@ -258,23 +246,23 @@ impl SelectedGradient { } /// Update the selected gradient, checking for removal or change of gradient. - pub fn update(gradient: &mut Option, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) { + pub fn update(gradient: &mut Option, document: &DocumentMessageHandler, responses: &mut VecDeque) { let Some(inner_gradient) = gradient else { return; }; // Clear the gradient if layer deleted - let Ok(layer) = document.document_legacy.layer(&inner_gradient.path) else { + if !inner_gradient.layer.exists(&document.document_legacy.metadata) { responses.add(ToolMessage::RefreshToolOptions); *gradient = None; return; }; // Update transform - inner_gradient.transform = gradient_space_transform(&inner_gradient.path, layer, document, render_data); + inner_gradient.transform = gradient_space_transform(inner_gradient.layer, document); // Clear if no longer a gradient - let Some(gradient) = layer.style().ok().and_then(|style| style.fill().as_gradient()) else { + let Some(gradient) = get_gradient(inner_gradient.layer, &document.document_legacy) else { responses.add(ToolMessage::RefreshToolOptions); *gradient = None; return; @@ -340,7 +328,7 @@ impl SelectedGradient { pub fn render_gradient(&mut self, responses: &mut VecDeque) { self.gradient.transform = self.transform; let fill = Fill::Gradient(self.gradient.clone()); - let layer = self.path.clone(); + let layer = self.layer.to_path(); responses.add(GraphOperationMessage::FillSet { layer, fill }); } } @@ -400,20 +388,16 @@ impl Fsm for GradientToolFsmState { } if self != GradientToolFsmState::Drawing { - SelectedGradient::update(&mut tool_data.selected_gradient, document, render_data, responses); + SelectedGradient::update(&mut tool_data.selected_gradient, document, responses); } - for path in document.selected_visible_layers() { - let layer = document.document_legacy.layer(path).unwrap(); - - if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) { + for layer in document.document_legacy.metadata.selected_visible_layers() { + if let Some(gradient) = get_gradient(layer, &document.document_legacy) { let dragging = tool_data .selected_gradient .as_ref() - .and_then(|selected| if selected.path == path { Some(selected.dragging) } else { None }); - tool_data - .gradient_overlays - .push(GradientOverlay::new(gradient, dragging, path, layer, document, responses, render_data)) + .and_then(|selected| if selected.layer == layer { Some(selected.dragging) } else { None }); + tool_data.gradient_overlays.push(GradientOverlay::new(gradient, dragging, layer, document, responses)) } } @@ -439,7 +423,7 @@ impl Fsm for GradientToolFsmState { // The gradient has only one point and so should become a fill if selected_gradient.gradient.positions.len() == 1 { let fill = Fill::Solid(selected_gradient.gradient.positions[0].1.unwrap_or(Color::BLACK)); - let layer = selected_gradient.path.clone(); + let layer = selected_gradient.layer.to_path(); responses.add(GraphOperationMessage::FillSet { layer, fill }); return self; } @@ -481,18 +465,15 @@ impl Fsm for GradientToolFsmState { if let Some(index) = gradient.insert_stop(mouse, overlay.transform) { document.backup_nonmut(responses); - let layer = document.document_legacy.layer(&overlay.path); - if let Ok(layer) = layer { - let mut selected_gradient = SelectedGradient::new(gradient, &overlay.path, layer, document, render_data); + let mut selected_gradient = SelectedGradient::new(gradient, overlay.layer, document); - // Select the new point - selected_gradient.dragging = GradientDragTarget::Step(index); + // Select the new point + selected_gradient.dragging = GradientDragTarget::Step(index); - // Update the layer fill - selected_gradient.render_gradient(responses); + // Update the layer fill + selected_gradient.render_gradient(responses); - tool_data.selected_gradient = Some(selected_gradient); - } + tool_data.selected_gradient = Some(selected_gradient); break; } @@ -516,7 +497,7 @@ impl Fsm for GradientToolFsmState { if pos.distance_squared(mouse) < tolerance { dragging = true; tool_data.selected_gradient = Some(SelectedGradient { - path: overlay.path.clone(), + layer: overlay.layer.clone(), transform: overlay.transform, gradient: overlay.gradient.clone(), dragging: GradientDragTarget::Step(index), @@ -533,7 +514,7 @@ impl Fsm for GradientToolFsmState { dragging = true; start_snap(&mut tool_data.snap_manager, document, input, render_data); tool_data.selected_gradient = Some(SelectedGradient { - path: overlay.path.clone(), + layer: overlay.layer.clone(), transform: overlay.transform, gradient: overlay.gradient.clone(), dragging: dragging_target, @@ -545,34 +526,30 @@ impl Fsm for GradientToolFsmState { document.backup_nonmut(responses); GradientToolFsmState::Drawing } else { - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]); - let intersection = document.document_legacy.intersects_quad_root(quad, render_data).pop(); + let selected_layer = document.document_legacy.metadata.click(input.mouse.position); - // the intersection is the layer where the gradient is being applied - if let Some(intersection) = intersection { - let is_bitmap = document - .document_legacy - .layer(&intersection) - .ok() - .and_then(|layer| layer.as_layer().ok()) - .map_or(false, |layer| matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_))); - if is_bitmap { - return self; - } + // Apply the gradient to the selected layer + if let Some((layer, _)) = selected_layer { + // let is_bitmap = document + // .document_legacy + // .layer(&layer) + // .ok() + // .and_then(|layer| layer.as_layer().ok()) + // .map_or(false, |layer| matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_))); + // if is_bitmap { + // return self; + // } - if !document.selected_layers_contains(&intersection) { - let replacement_selected_layers = vec![intersection.clone()]; + if !document.document_legacy.metadata.selected_layers_contains(layer) { + let replacement_selected_layers = vec![layer.to_path()]; responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers }); } - let layer = document.document_legacy.layer(&intersection).unwrap(); - responses.add(DocumentMessage::StartTransaction); // Use the already existing gradient if it exists - let gradient = if let Some(gradient) = layer.style().ok().map(|style| style.fill()).and_then(|fill| fill.as_gradient()) { + let gradient = if let Some(gradient) = get_gradient(layer, &document.document_legacy) { gradient.clone() } else { // Generate a new gradient @@ -586,7 +563,7 @@ impl Fsm for GradientToolFsmState { tool_options.gradient_type, ) }; - let selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document, render_data).with_gradient_start(input.mouse.position); + let selected_gradient = SelectedGradient::new(gradient, layer, document).with_gradient_start(input.mouse.position); tool_data.selected_gradient = Some(selected_gradient); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 2319fbdd..b7ec3417 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -3,14 +3,11 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths; use crate::messages::tool::common_functionality::snapping::SnapManager; -use crate::messages::tool::tool_messages::pen_tool::graph_modification_utils::NodeGraphLayer; -use bezier_rs::Subpath; -use document_legacy::document::Document; use document_legacy::document_metadata::LayerNodeIdentifier; use document_legacy::LayerId; -use graph_craft::document::value::TaggedValue; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::{ManipulatorPointId, SelectedType}; @@ -767,26 +764,3 @@ fn should_extend(document: &DocumentMessageHandler, pos: DVec2, tolerance: f64) best } - -pub fn get_subpaths(layer: LayerNodeIdentifier, document: &Document) -> Option<&Vec>> { - if let TaggedValue::Subpaths(subpaths) = NodeGraphLayer::new(layer, document)?.find_input("Shape", 0)? { - Some(subpaths) - } else { - None - } -} - -pub fn get_mirror_handles(layer: LayerNodeIdentifier, document: &Document) -> Option<&Vec> { - if let TaggedValue::ManipulatorGroupIds(mirror_handles) = NodeGraphLayer::new(layer, document)?.find_input("Shape", 1)? { - Some(mirror_handles) - } else { - None - } -} - -pub fn get_manipulator_groups(subpaths: &[Subpath]) -> impl Iterator> + DoubleEndedIterator { - subpaths.iter().flat_map(|subpath| subpath.manipulator_groups()) -} -pub fn get_manipulator_from_id(subpaths: &[Subpath], id: ManipulatorGroupId) -> Option<&bezier_rs::ManipulatorGroup> { - subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id)) -} diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index a5267d17..862d05bd 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -5,7 +5,7 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::snapping::SnapManager; -use document_legacy::{LayerId, Operation}; +use document_legacy::LayerId; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::Color; diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 2ddec7e2..7a3ccfaa 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -239,6 +239,13 @@ impl NodeInput { NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"), } } + pub fn as_value(&self) -> Option<&TaggedValue> { + if let NodeInput::Value { tagged_value, .. } = self { + Some(tagged_value) + } else { + None + } + } } #[derive(Clone, Debug, PartialEq, Hash, DynAny)]