use crate::{
asset::item::AssetItem,
fyrox::{
core::pool::{ErasedHandle, Handle},
graph::SceneGraph,
gui::{
border::BorderBuilder,
button::{ButtonBuilder, ButtonMessage},
decorator::{Decorator, DecoratorBuilder, DecoratorMessage},
grid::{Column, GridBuilder, Row},
image::ImageBuilder,
message::{MessageDirection, UiMessage},
scroll_viewer::{ScrollViewerBuilder, ScrollViewerMessage},
searchbar::{SearchBarBuilder, SearchBarMessage},
stack_panel::StackPanelBuilder,
style::{resource::StyleResourceExt, Style},
text::TextBuilder,
toggle::{ToggleButtonBuilder, ToggleButtonMessage},
tree::{
TreeBuilder, TreeExpansionStrategy, TreeMessage, TreeRoot, TreeRootBuilder,
TreeRootMessage,
},
utils::make_simple_tooltip,
widget::{WidgetBuilder, WidgetMessage},
window::{WindowBuilder, WindowTitle},
wrap_panel::WrapPanelBuilder,
BuildContext, HorizontalAlignment, Orientation, RcUiNodeHandle, Thickness, UiNode,
UserInterface, VerticalAlignment,
},
resource::texture::TextureResource,
},
load_image,
message::MessageSender,
utils::window_content,
world::item::{DropAnchor, SceneItem, SceneItemBuilder, SceneItemMessage},
Mode, Settings,
};
use fyrox::core::color::Color;
use fyrox::core::pool::HandlesVecExtension;
use fyrox::gui::button::Button;
use fyrox::gui::scroll_viewer::ScrollViewer;
use fyrox::gui::searchbar::SearchBar;
use fyrox::gui::text_box::EmptyTextPlaceholder;
use fyrox::gui::toggle::ToggleButton;
use fyrox::gui::tree::Tree;
use fyrox::gui::utils::ImageButtonBuilder;
use fyrox::gui::window::Window;
use fyrox::gui::wrap_panel::WrapPanel;
use rust_fuzzy_search::fuzzy_compare;
use std::{
borrow::Cow,
cell::RefCell,
collections::HashMap,
ops::Deref,
path::{Path, PathBuf},
rc::Rc,
};
pub mod graph;
pub mod item;
pub mod menu;
pub mod selection;
pub struct SceneItemIcon {
pub icon: TextureResource,
pub color: Color,
}
#[macro_export]
macro_rules! scene_item_icon {
($icon:expr,$color:expr) => {
$crate::load_image!($icon).map(|icon| SceneItemIcon {
icon,
color: $color,
})
};
}
pub trait WorldViewerDataProvider {
fn root_node(&self) -> ErasedHandle;
fn path(&self) -> Option<&Path>;
fn children_of(&self, node: ErasedHandle) -> Vec<ErasedHandle>;
fn child_count_of(&self, node: ErasedHandle) -> usize;
fn nth_child(&self, node: ErasedHandle, i: usize) -> ErasedHandle;
fn is_node_has_child(&self, node: ErasedHandle, child: ErasedHandle) -> bool;
fn parent_of(&self, node: ErasedHandle) -> ErasedHandle;
fn name_of(&self, node: ErasedHandle) -> Option<Cow<str>>;
fn is_valid_handle(&self, node: ErasedHandle) -> bool;
fn icon_of(&self, node: ErasedHandle) -> Option<SceneItemIcon>;
fn is_instance(&self, node: ErasedHandle) -> bool;
fn selection(&self) -> Vec<ErasedHandle>;
fn on_change_hierarchy_request(
&self,
child: ErasedHandle,
parent: ErasedHandle,
anchor: DropAnchor,
);
fn on_asset_dropped(&mut self, path: PathBuf, node: ErasedHandle);
fn validate(&self) -> Vec<(ErasedHandle, Result<(), String>)>;
fn on_selection_changed(&self, new_selection: &[ErasedHandle]);
}
pub trait WorldViewerItemContextMenu {
fn menu(&self) -> RcUiNodeHandle;
}
pub struct WorldViewer {
pub window: Handle<Window>,
tree_root: Handle<TreeRoot>,
sender: MessageSender,
track_selection: Handle<ToggleButton>,
search_bar: Handle<SearchBar>,
filter: String,
stack: Vec<(Handle<UiNode>, ErasedHandle)>,
pub sync_selection: bool,
node_path: Handle<WrapPanel>,
breadcrumbs: HashMap<Handle<Button>, Handle<UiNode>>,
collapse_all: Handle<Button>,
expand_all: Handle<Button>,
locate_selection: Handle<Button>,
scroll_view: Handle<ScrollViewer>,
pub item_context_menu: Option<Rc<RefCell<dyn WorldViewerItemContextMenu>>>,
node_to_view_map: HashMap<ErasedHandle, Handle<SceneItem>>,
}
fn make_graph_node_item(
name: Cow<str>,
is_instance: bool,
icon: Option<SceneItemIcon>,
handle: ErasedHandle,
ctx: &mut BuildContext,
context_menu: RcUiNodeHandle,
sender: MessageSender,
is_expanded: bool,
) -> Handle<SceneItem> {
SceneItemBuilder::new(
TreeBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::left(1.0))
.with_context_menu(context_menu),
)
.with_expanded(is_expanded),
)
.with_text_brush(if is_instance {
ctx.style.property(WorldViewer::INSTANCE_BRUSH)
} else {
ctx.style.property(Style::BRUSH_TEXT)
})
.with_name(name.deref().to_owned())
.with_entity_handle(handle)
.with_icon(icon)
.build(ctx, sender)
}
fn tree_node(ui: &UserInterface, tree: Handle<SceneItem>) -> ErasedHandle {
ui[tree].entity_handle
}
fn colorize(handle: Handle<UiNode>, ui: &UserInterface, index: &mut usize) {
let node = ui.node(handle);
if let Some(decorator) = node.cast::<Decorator>() {
if node.parent().is_some() {
let new_brush = if (*index).is_multiple_of(2) {
ui.style.property(Style::BRUSH_PRIMARY)
} else {
ui.style.property(Style::BRUSH_LIGHTER_PRIMARY)
};
if *decorator.normal_brush != new_brush {
ui.send(handle, DecoratorMessage::NormalBrush(new_brush));
}
*index += 1;
}
}
for &item in node.children() {
colorize(item, ui, index);
}
}
fn fetch_expanded_state(
node: ErasedHandle,
data_provider: &dyn WorldViewerDataProvider,
settings: &Settings,
) -> bool {
data_provider
.path()
.as_ref()
.and_then(|p| settings.scene_settings.get(*p))
.and_then(|s| s.node_infos.get(&node))
.is_none_or(|i| i.is_expanded)
}
impl WorldViewer {
pub const INSTANCE_BRUSH: &'static str = "WorldViewer.InstanceBrush";
pub fn new(ctx: &mut BuildContext, sender: MessageSender, settings: &Settings) -> Self {
let tree_root;
let node_path;
let collapse_all;
let expand_all;
let locate_selection;
let scroll_view;
let search_bar = SearchBarBuilder::new(
WidgetBuilder::new()
.with_tab_index(Some(4))
.on_row(0)
.on_column(1)
.with_margin(Thickness::uniform(2.0)),
)
.with_empty_text_placeholder(EmptyTextPlaceholder::Text("Search for an object"))
.build(ctx);
let track_selection_tooltip = make_simple_tooltip(
ctx,
"Track selection. If enabled, \
then the world viewer will automatically scroll to the \
selected object. If multiple objects are selected then \
first one will be brought into view.",
);
let track_selection = ToggleButtonBuilder::new(
WidgetBuilder::new()
.with_tab_index(Some(3))
.with_vertical_alignment(VerticalAlignment::Center)
.with_margin(Thickness::uniform(1.0))
.with_width(26.0)
.with_height(26.0)
.with_tooltip(track_selection_tooltip),
)
.with_content(
ImageBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_width(13.0)
.with_height(17.0)
.with_horizontal_alignment(HorizontalAlignment::Center)
.with_vertical_alignment(VerticalAlignment::Center),
)
.with_opt_texture(load_image!("../../resources/track.png"))
.build(ctx),
)
.with_toggled(settings.selection.track_selection)
.build(ctx);
let buttons = StackPanelBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.on_row(0)
.on_column(0)
.with_child({
collapse_all = ImageButtonBuilder::default()
.with_image(load_image!("../../resources/collapse.png"))
.with_tooltip("Collapse Everything")
.with_tab_index(Some(0))
.build_button(ctx);
collapse_all
})
.with_child({
expand_all = ImageButtonBuilder::default()
.with_image(load_image!("../../resources/expand.png"))
.with_tooltip("Expand Everything")
.with_tab_index(Some(1))
.build_button(ctx);
expand_all
})
.with_child({
locate_selection = ImageButtonBuilder::default()
.with_image(load_image!("../../resources/locate.png"))
.with_tooltip("Locate Selection")
.with_tab_index(Some(2))
.build_button(ctx);
locate_selection
})
.with_child(track_selection),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let toolbar = GridBuilder::new(
WidgetBuilder::new()
.with_child(buttons)
.with_child(search_bar),
)
.add_row(Row::auto())
.add_column(Column::auto())
.add_column(Column::stretch())
.build(ctx);
let window = WindowBuilder::new(WidgetBuilder::new().with_name("WorldOutliner"))
.with_title(WindowTitle::text("World Viewer"))
.with_tab_label("World")
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child(toolbar)
.with_child({
scroll_view = ScrollViewerBuilder::new(WidgetBuilder::new().on_row(1))
.with_content({
tree_root = TreeRootBuilder::new(
WidgetBuilder::new().with_tab_index(Some(5)),
)
.build(ctx);
tree_root
})
.build(ctx);
scroll_view
})
.with_child({
node_path = WrapPanelBuilder::new(
WidgetBuilder::new()
.on_row(2)
.with_vertical_alignment(VerticalAlignment::Top),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
node_path
}),
)
.add_column(Column::stretch())
.add_row(Row::auto())
.add_row(Row::stretch())
.add_row(Row::auto())
.build(ctx),
)
.build(ctx);
Self {
search_bar,
track_selection,
window,
sender,
tree_root,
node_path,
stack: Default::default(),
sync_selection: false,
breadcrumbs: Default::default(),
locate_selection,
collapse_all,
expand_all,
scroll_view,
item_context_menu: None,
node_to_view_map: Default::default(),
filter: Default::default(),
}
}
pub fn sync_to_model(
&mut self,
data_provider: &dyn WorldViewerDataProvider,
ui: &mut UserInterface,
settings: &Settings,
) {
self.sync_graph(ui, data_provider, settings);
self.validate(data_provider, ui);
}
fn build_breadcrumb(
&mut self,
name: &str,
associated_item: Handle<UiNode>,
ui: &mut UserInterface,
) {
let ctx = &mut ui.build_ctx();
let element = ButtonBuilder::new(WidgetBuilder::new().with_height(16.0))
.with_back(
DecoratorBuilder::new(BorderBuilder::new(
WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_PRIMARY)),
))
.with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
.with_hover_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
.build(ctx),
)
.with_content(
TextBuilder::new(WidgetBuilder::new())
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(if self.breadcrumbs.is_empty() {
name.to_owned()
} else {
format!("{name} >")
})
.with_font_size(11.0.into())
.build(ctx),
)
.build(ctx);
ui.send_sync(element, WidgetMessage::link_with_reverse(self.node_path));
self.breadcrumbs.insert(element, associated_item);
}
fn clear_breadcrumbs(&mut self, ui: &UserInterface) {
self.breadcrumbs.clear();
for &child in ui[self.node_path].children() {
ui.send_sync(child, WidgetMessage::Remove);
}
}
fn update_breadcrumbs(
&mut self,
ui: &mut UserInterface,
data_provider: &dyn WorldViewerDataProvider,
) {
self.clear_breadcrumbs(ui);
if let Some(&first_selected) = data_provider.selection().first() {
let mut node_handle = first_selected;
while node_handle.is_some() && node_handle != data_provider.root_node() {
let view = ui.find_handle(self.tree_root, &mut |n| {
n.cast::<SceneItem>()
.map(|i| i.entity_handle == node_handle)
.unwrap_or_default()
});
if view.is_some() {
self.build_breadcrumb(
&format!(
"{}({})",
data_provider.name_of(node_handle).unwrap_or_default(),
node_handle
),
view,
ui,
);
}
node_handle = data_provider.parent_of(node_handle);
}
}
}
fn sync_graph(
&mut self,
ui: &mut UserInterface,
data_provider: &dyn WorldViewerDataProvider,
settings: &Settings,
) {
self.stack.clear();
self.stack
.push((self.tree_root.to_base(), data_provider.root_node()));
while let Some((tree_handle, node_handle)) = self.stack.pop() {
let ui_node = ui.node(tree_handle);
if let Some(item) = ui_node.cast::<SceneItem>() {
let mut items = item
.tree
.items
.iter()
.map(|i| i.transmute::<SceneItem>())
.collect::<Vec<_>>();
let mut i = 0;
while i < items.len() {
let item = items[i];
let child_node = tree_node(ui, item);
if !data_provider.is_node_has_child(node_handle, child_node) {
ui.send_sync(tree_handle, TreeMessage::RemoveItem(item.transmute()));
if let Some(existing_view) = self.node_to_view_map.get(&child_node) {
if *existing_view == item {
self.node_to_view_map.remove(&child_node);
}
}
items.remove(i);
} else {
i += 1;
}
}
for child_handle in data_provider.children_of(node_handle) {
let mut found = false;
for &item in items.iter() {
let tree_node_handle = tree_node(ui, item);
if tree_node_handle == child_handle {
found = true;
break;
}
}
if !found {
let menu = self.item_context_menu.as_ref().map_or(
RcUiNodeHandle::new(Handle::<UiNode>::default(), ui.sender()),
|menu| menu.borrow().menu(),
);
let graph_node_item = make_graph_node_item(
data_provider.name_of(child_handle).unwrap_or_default(),
data_provider.is_instance(child_handle),
data_provider.icon_of(child_handle),
child_handle,
&mut ui.build_ctx(),
menu,
self.sender.clone(),
fetch_expanded_state(child_handle, data_provider, settings),
);
ui.send_sync(
tree_handle,
TreeMessage::AddItem(graph_node_item.transmute()),
);
items.push(graph_node_item);
self.node_to_view_map.insert(child_handle, graph_node_item);
}
}
for &tree in items.iter() {
let child = tree_node(ui, tree);
self.stack.push((tree.to_base(), child));
}
{
let mut is_order_match = true;
for (i, &child_tree) in items.iter().enumerate() {
let nth_child = data_provider.nth_child(node_handle, i);
if nth_child != tree_node(ui, child_tree) {
is_order_match = false;
break;
}
}
if !is_order_match {
ui.send(
tree_handle,
TreeMessage::SetItems {
items: data_provider
.children_of(node_handle)
.into_iter()
.map(|c| self.node_to_view_map.get(&c).cloned().unwrap())
.collect::<Vec<_>>()
.to_any(),
remove_previous: false,
},
);
}
}
} else if let Some(tree_root) = ui_node.cast::<TreeRoot>() {
if tree_root.items.is_empty()
|| tree_node(ui, tree_root.items[0].transmute()) != data_provider.root_node()
{
let menu = self.item_context_menu.as_ref().map_or(
RcUiNodeHandle::new(Handle::<UiNode>::default(), ui.sender()),
|menu| menu.borrow().menu(),
);
let new_root_item = make_graph_node_item(
data_provider.name_of(node_handle).unwrap_or_default(),
data_provider.is_instance(node_handle),
data_provider.icon_of(node_handle),
node_handle,
&mut ui.build_ctx(),
menu,
self.sender.clone(),
fetch_expanded_state(node_handle, data_provider, settings),
);
ui.send_sync(
tree_handle,
TreeRootMessage::Items(vec![new_root_item.transmute()]),
);
self.node_to_view_map.insert(node_handle, new_root_item);
self.stack.push((new_root_item.transmute(), node_handle));
} else {
self.stack
.push((tree_root.items[0].transmute(), node_handle));
}
}
}
let mut stack = vec![self.tree_root.to_base()];
while let Some(handle) = stack.pop() {
let ui_node = ui.node(handle);
if let Some(item) = ui_node.cast::<SceneItem>() {
if let Some(name) = data_provider.name_of(item.entity_handle) {
if item.name() != name {
ui.send_sync(handle, SceneItemMessage::Name((*name).to_owned()));
}
stack.extend(item.tree.items.iter().map(|v| v.to_base()));
}
} else if let Some(root) = ui_node.cast::<TreeRoot>() {
stack.extend(root.items.iter().map(|v| v.to_base()))
}
}
self.colorize(ui);
self.node_to_view_map
.retain(|k, v| data_provider.is_valid_handle(*k) && ui.try_get(*v).is_ok());
}
pub fn colorize(&mut self, ui: &UserInterface) {
let mut index = 0;
colorize(self.tree_root.to_base(), ui, &mut index);
}
fn apply_filter(&self, data_provider: &dyn WorldViewerDataProvider, ui: &UserInterface) {
fn apply_filter_recursive(node: Handle<UiNode>, filter: &str, ui: &UserInterface) -> bool {
let node_ref = ui.node(node);
let mut is_any_match = false;
for &child in node_ref.children() {
is_any_match |= apply_filter_recursive(child, filter, ui)
}
let name = node_ref.cast::<SceneItem>().map(|i| i.name());
if let Some(name) = name {
is_any_match |= name.to_lowercase().contains(filter)
|| fuzzy_compare(filter, name.to_lowercase().as_str()) >= 0.33;
ui.send(node, WidgetMessage::Visibility(is_any_match));
}
is_any_match
}
apply_filter_recursive(self.tree_root.to_base(), &self.filter.to_lowercase(), ui);
if self.filter.is_empty() {
if let Some(first) = data_provider.selection().first() {
if let Some(view) = self.node_to_view_map.get(first) {
ui.send(
self.scroll_view,
ScrollViewerMessage::BringIntoView(view.to_base()),
);
}
}
}
}
pub fn set_filter(
&mut self,
filter: String,
data_provider: &dyn WorldViewerDataProvider,
ui: &UserInterface,
) {
self.filter = filter;
self.apply_filter(data_provider, ui)
}
pub fn handle_ui_message(
&mut self,
message: &UiMessage,
data_provider: &mut dyn WorldViewerDataProvider,
ui: &UserInterface,
settings: &mut Settings,
) {
if let Some(TreeRootMessage::Select(selection)) = message.data::<TreeRootMessage>() {
if message.destination() == self.tree_root
&& message.direction() == MessageDirection::FromWidget
{
self.handle_selection(selection, data_provider, ui);
}
} else if let Some(&WidgetMessage::Drop(node)) = message.data::<WidgetMessage>() {
self.handle_drop(ui, data_provider, message.destination(), node);
} else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
if let Some(&view) = self.breadcrumbs.get(&message.destination().to_variant()) {
if let Ok(graph_node) = ui.try_get_of_type::<SceneItem>(view) {
data_provider.on_selection_changed(&[graph_node.entity_handle]);
}
} else if message.destination() == self.collapse_all {
ui.send(self.tree_root, TreeRootMessage::CollapseAll);
} else if message.destination() == self.expand_all {
ui.send(self.tree_root, TreeRootMessage::ExpandAll);
} else if message.destination() == self.locate_selection {
self.locate_selection(&data_provider.selection(), ui)
}
} else if let Some(ToggleButtonMessage::Toggled(value)) = message.data() {
if message.destination() == self.track_selection {
settings.selection.track_selection = *value;
if *value {
self.locate_selection(&data_provider.selection(), ui);
}
}
} else if let Some(SearchBarMessage::Text(text)) = message.data() {
if message.destination() == self.search_bar
&& message.direction == MessageDirection::FromWidget
{
self.set_filter(text.clone(), data_provider, ui);
}
} else if let Some(TreeMessage::Expand { expand, .. }) = message.data() {
if let Some(scene_view_item) = ui
.node(message.destination())
.query_component::<SceneItem>()
{
if let Some(path) = data_provider.path() {
settings
.scene_settings
.entry(path.to_owned())
.or_default()
.node_infos
.entry(scene_view_item.entity_handle)
.or_default()
.is_expanded = *expand;
}
}
}
}
pub fn try_locate_object(&self, handle: ErasedHandle, ui: &UserInterface) {
self.locate_selection(&[handle], ui)
}
fn locate_selection(&self, selection: &[ErasedHandle], ui: &UserInterface) {
let tree_to_focus = self.map_selection(selection, ui);
if let Some(tree_to_focus) = tree_to_focus.first() {
ui.send(
*tree_to_focus,
TreeMessage::Expand {
expand: true,
expansion_strategy: TreeExpansionStrategy::RecursiveAncestors,
},
);
ui.send(
self.scroll_view,
ScrollViewerMessage::BringIntoView(tree_to_focus.to_base()),
);
}
}
fn handle_selection(
&self,
selection: &[Handle<Tree>],
data_provider: &dyn WorldViewerDataProvider,
ui: &UserInterface,
) {
data_provider.on_selection_changed(
&selection
.iter()
.map(|selected_item| ui[selected_item.transmute::<SceneItem>()].entity_handle)
.collect::<Vec<_>>(),
);
}
fn handle_drop(
&self,
ui: &UserInterface,
data_provider: &mut dyn WorldViewerDataProvider,
target: Handle<UiNode>,
dropped: Handle<UiNode>,
) {
if let Some(item) = ui.node(dropped).cast::<AssetItem>() {
if let Some(parent) = ui.node(target).cast::<SceneItem>() {
data_provider.on_asset_dropped(item.path.clone(), parent.entity_handle);
}
} else if ui.is_node_child_of(dropped, self.tree_root)
&& ui.is_node_child_of(target, self.tree_root)
&& dropped != target
{
if let (Some(child), Some(parent)) = (
ui.node(dropped).cast::<SceneItem>(),
ui.node(target).cast::<SceneItem>(),
) {
data_provider.on_change_hierarchy_request(
child.entity_handle,
parent.entity_handle,
parent.drop_anchor,
)
}
}
}
fn map_selection(
&self,
selection: &[ErasedHandle],
ui: &UserInterface,
) -> Vec<Handle<SceneItem>> {
map_selection(selection, self.tree_root.to_base(), ui)
}
pub fn post_update(
&mut self,
data_provider: &dyn WorldViewerDataProvider,
ui: &mut UserInterface,
settings: &Settings,
) {
if self.sync_selection {
let trees = self.map_selection(&data_provider.selection(), ui).to_any();
ui.send_sync(self.tree_root, TreeRootMessage::Select(trees));
self.update_breadcrumbs(ui, data_provider);
if settings.selection.track_selection {
self.locate_selection(&data_provider.selection(), ui);
}
self.sync_selection = false;
}
}
pub fn clear(&mut self, ui: &UserInterface) {
self.node_to_view_map.clear();
self.clear_breadcrumbs(ui);
ui.send(self.tree_root, TreeRootMessage::Items(vec![]));
}
pub fn on_configure(&self, ui: &UserInterface, settings: &Settings) {
ui.send(
self.track_selection,
ToggleButtonMessage::Toggled(settings.selection.track_selection),
);
}
pub fn on_mode_changed(&mut self, ui: &UserInterface, mode: &Mode) {
ui.send(
window_content(self.window, ui),
WidgetMessage::Enabled(mode.is_edit()),
);
}
pub fn validate(&self, data_provider: &dyn WorldViewerDataProvider, ui: &UserInterface) {
for (node_handle, result) in data_provider.validate() {
if let Some(view) = self.node_to_view_map.get(&node_handle) {
let view_ref = &ui[*view];
if view_ref.warning_icon.is_none() && result.is_err()
|| view_ref.warning_icon.is_some() && result.is_ok()
{
ui.send_sync(*view, SceneItemMessage::Validate(result));
}
}
}
}
}
fn map_selection(
selection: &[ErasedHandle],
root_node: Handle<UiNode>,
ui: &UserInterface,
) -> Vec<Handle<SceneItem>> {
selection
.iter()
.filter_map(|&handle| {
let item = ui
.find_handle(root_node, &mut |n| {
n.cast::<SceneItem>()
.is_some_and(|n| n.entity_handle == handle)
})
.to_variant();
if item.is_some() {
Some(item)
} else {
None
}
})
.collect()
}