diff --git a/editor/src/plugins/inspector/editors/surface.rs b/editor/src/plugins/inspector/editors/surface.rs index 8c471703d..15589f887 100644 --- a/editor/src/plugins/inspector/editors/surface.rs +++ b/editor/src/plugins/inspector/editors/surface.rs @@ -207,15 +207,19 @@ fn surface_data_info(resource_manager: &ResourceManager, data: &SurfaceResource) .resource_path(data.as_ref()) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "External".to_string()); - let guard = data.data_ref(); - format!( - "{}\nVertices: {}\nTriangles: {}\nUse Count: {}\nId: {}", - kind, - guard.vertex_buffer.vertex_count(), - guard.geometry_buffer.len(), - use_count, - id - ) + if data.is_ok() { + let guard = data.data_ref(); + format!( + "{}\nVertices: {}\nTriangles: {}\nUse Count: {}\nId: {}", + kind, + guard.vertex_buffer.vertex_count(), + guard.geometry_buffer.len(), + use_count, + id + ) + } else { + format!("{}\nNot loaded\nUse Count: {}\nId: {}", kind, use_count, id) + } } impl SurfaceDataPropertyEditor { diff --git a/fyrox-core/src/io.rs b/fyrox-core/src/io.rs index 211b6b4f4..027384c3e 100644 --- a/fyrox-core/src/io.rs +++ b/fyrox-core/src/io.rs @@ -27,6 +27,8 @@ pub enum FileError { Custom(String), } +impl std::error::Error for FileError {} + impl Display for FileError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/fyrox-core/src/pool/mod.rs b/fyrox-core/src/pool/mod.rs index 0769e8e85..f5554b515 100644 --- a/fyrox-core/src/pool/mod.rs +++ b/fyrox-core/src/pool/mod.rs @@ -64,7 +64,6 @@ const INVALID_GENERATION: u32 = 0; /// block. It allows to create and delete objects much faster than if they'll /// be allocated on heap. Also since objects stored in contiguous memory block /// they can be effectively accessed because such memory layout is cache-friendly. -#[derive(Debug)] pub struct Pool> where T: Sized, @@ -74,6 +73,16 @@ where free_stack: Vec, } +impl + 'static> Debug for Pool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("Pool"); + for (handle, value) in self.pair_iter() { + s.field(&handle.to_string(), value); + } + s.finish() + } +} + /// This trait unifies pool objects and their variants. /// /// If T is the pool object type, [`ObjectOrVariant::convert_to_dest_type`] returns the pool object itself. diff --git a/fyrox-core/src/visitor/error.rs b/fyrox-core/src/visitor/error.rs index 8c8c25791..0043fd6b6 100644 --- a/fyrox-core/src/visitor/error.rs +++ b/fyrox-core/src/visitor/error.rs @@ -21,6 +21,7 @@ //! Possible errors that may occur during serialization/deserialization. use crate::io::FileError; +use crate::visitor::Visitor; use base64::DecodeError; use std::num::{ParseFloatError, ParseIntError}; use std::{ @@ -32,6 +33,9 @@ use std::{ /// Errors that may occur while reading or writing [`crate::visitor::Visitor`]. #[derive(Debug)] pub enum VisitError { + /// An error that occured for multiple reasons, when there are multiple potential ways + /// to visit a node, and all of them lead to errors. + Multiple(Vec), /// An [std::io::Error] occured while reading or writing a file with Visitor data. Io(std::io::Error), /// When a field is encoded as bytes, the field data is prefixed by an identifying byte @@ -72,7 +76,12 @@ pub enum VisitError { /// to a single shared value. This causes the visitor to store the data once and then later references /// to the same value point back to its first occurrence. This error occurs if one of these references /// points to a value of the wrong type. - TypeMismatch, + TypeMismatch { + /// The type that was visiting when the error occurred. + expected: &'static str, + /// The type that was stored in the `Rc` or `Arc`. + actual: &'static str, + }, /// Attempting to visit a mutably borrowed RefCell. RefCellAlreadyMutableBorrowed, /// A plain-text error message that could indicate almost anything. @@ -96,12 +105,45 @@ pub enum VisitError { impl Error for VisitError {} +impl VisitError { + /// Create a [`VisitError::FieldDoesNotExist`] containing the given field name and the + /// breadcrumbs of the current visitor node. + pub fn field_does_not_exist(name: &str, visitor: &Visitor) -> Self { + Self::FieldDoesNotExist(visitor.breadcrumbs() + " > " + name) + } + /// Create an error from two errors. + pub fn multiple(self, other: Self) -> Self { + match (self, other) { + (Self::Multiple(mut a), Self::Multiple(mut b)) => { + a.append(&mut b); + Self::Multiple(a) + } + (Self::Multiple(mut a), b) => { + a.push(b); + Self::Multiple(a) + } + (a, Self::Multiple(mut b)) => { + b.push(a); + Self::Multiple(b) + } + (a, b) => Self::Multiple(vec![a, b]), + } + } +} + impl Display for VisitError { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { + Self::Multiple(errs) => { + write!(f, "multiple errors:[")?; + for err in errs { + write!(f, "{err};")?; + } + write!(f, "]") + } Self::Io(io) => write!(f, "io error: {io}"), Self::UnknownFieldType(type_index) => write!(f, "unknown field type {type_index}"), - Self::FieldDoesNotExist(name) => write!(f, "field does not exists {name}"), + Self::FieldDoesNotExist(name) => write!(f, "field does not exist: {name}"), Self::FieldAlreadyExists(name) => write!(f, "field already exists {name}"), Self::RegionAlreadyExists(name) => write!(f, "region already exists {name}"), Self::InvalidCurrentNode => write!(f, "invalid current node"), @@ -113,7 +155,9 @@ impl Display for VisitError { Self::NoActiveNode => write!(f, "no active node"), Self::NotSupportedFormat => write!(f, "not supported format"), Self::InvalidName => write!(f, "invalid name"), - Self::TypeMismatch => write!(f, "type mismatch"), + Self::TypeMismatch { expected, actual } => { + write!(f, "type mismatch. expected: {expected}, actual: {actual}") + } Self::RefCellAlreadyMutableBorrowed => write!(f, "ref cell already mutable borrowed"), Self::User(msg) => write!(f, "user defined error: {msg}"), Self::UnexpectedRcNullIndex => write!(f, "unexpected rc null index"), diff --git a/fyrox-core/src/visitor/impls.rs b/fyrox-core/src/visitor/impls.rs index 3cd5c8567..10dd8ac39 100644 --- a/fyrox-core/src/visitor/impls.rs +++ b/fyrox-core/src/visitor/impls.rs @@ -56,7 +56,7 @@ macro_rules! impl_visit_as_field { }), } } else { - Err(VisitError::FieldDoesNotExist(name.to_owned())) + Err(VisitError::field_does_not_exist(name, visitor)) } } else if visitor.find_field(name).is_some() { Err(VisitError::FieldAlreadyExists(name.to_owned())) @@ -240,7 +240,7 @@ impl Visit for String { }), } } else { - Err(VisitError::FieldDoesNotExist(name.to_owned())) + Err(VisitError::field_does_not_exist(name, visitor)) } } else if visitor.find_field(name).is_some() { Err(VisitError::FieldAlreadyExists(name.to_owned())) @@ -319,11 +319,23 @@ where if let Ok(res) = Rc::downcast::(ptr.clone()) { *self = res; } else { - return Err(VisitError::TypeMismatch); + return Err(VisitError::TypeMismatch { + expected: std::any::type_name::(), + actual: region.type_name_map.get(&id).unwrap_or(&"MISSING"), + }); } } else { + region.type_name_map.insert(id, std::any::type_name::()); region.rc_map.insert(id, self.clone()); - unsafe { rc_to_raw(self).visit("RcData", &mut region)? }; + let result = unsafe { rc_to_raw(self).visit("RcData", &mut region) }; + // Sometimes visiting is done experimentally, just to see if it would succeed, and visiting continues along a different + // path on failure. This means that the visitor must be in a valid state even after a failure, so we must remove + // the invalid rc_map entry if visiting failed. + if result.is_err() { + region.type_name_map.remove(&id); + region.rc_map.remove(&id); + return result; + } } } else { let (mut id, serialize_data) = region.rc_id(self); @@ -410,11 +422,23 @@ where if let Ok(res) = Arc::downcast::(ptr.clone()) { *self = res; } else { - return Err(VisitError::TypeMismatch); + return Err(VisitError::TypeMismatch { + expected: std::any::type_name::(), + actual: region.type_name_map.get(&id).unwrap_or(&"MISSING"), + }); } } else { + region.type_name_map.insert(id, std::any::type_name::()); region.arc_map.insert(id, self.clone()); - unsafe { arc_to_raw(self).visit("ArcData", &mut region)? }; + let result = unsafe { arc_to_raw(self).visit("ArcData", &mut region) }; + // Sometimes visiting is done experimentally, just to see if it would succeed, and visiting continues along a different + // path on failure. This means that the visitor must be in a valid state even after a failure, so we must remove + // the invalid arc_map entry if visiting failed. + if result.is_err() { + region.type_name_map.remove(&id); + region.arc_map.remove(&id); + return result; + } } } else { let (mut id, serialize_data) = region.arc_id(self); @@ -444,14 +468,26 @@ where if let Ok(res) = Rc::downcast::(ptr.clone()) { *self = Rc::downgrade(&res); } else { - return Err(VisitError::TypeMismatch); + return Err(VisitError::TypeMismatch { + expected: std::any::type_name::(), + actual: region.type_name_map.get(&id).unwrap_or(&"MISSING"), + }); } } else { // Create new value wrapped into Rc and deserialize it. let rc = Rc::new(T::default()); + region.type_name_map.insert(id, std::any::type_name::()); region.rc_map.insert(id, rc.clone()); - unsafe { rc_to_raw(&rc).visit("RcData", &mut region)? }; + let result = unsafe { rc_to_raw(&rc).visit("RcData", &mut region) }; + // Sometimes visiting is done experimentally, just to see if it would succeed, and visiting continues along a different + // path on failure. This means that the visitor must be in a valid state even after a failure, so we must remove + // the invalid rc_map entry if visiting failed. + if result.is_err() { + region.type_name_map.remove(&id); + region.rc_map.remove(&id); + return result; + } *self = Rc::downgrade(&rc); } @@ -487,12 +523,24 @@ where if let Ok(res) = Arc::downcast::(ptr.clone()) { *self = Arc::downgrade(&res); } else { - return Err(VisitError::TypeMismatch); + return Err(VisitError::TypeMismatch { + expected: std::any::type_name::(), + actual: region.type_name_map.get(&id).unwrap_or(&"MISSING"), + }); } } else { let arc = Arc::new(T::default()); + region.type_name_map.insert(id, std::any::type_name::()); region.arc_map.insert(id, arc.clone()); - unsafe { arc_to_raw(&arc).visit("ArcData", &mut region)? }; + let result = unsafe { arc_to_raw(&arc).visit("ArcData", &mut region) }; + // Sometimes visiting is done experimentally, just to see if it would succeed, and visiting continues along a different + // path on failure. This means that the visitor must be in a valid state even after a failure, so we must remove + // the invalid arc_map entry if visiting failed. + if result.is_err() { + region.type_name_map.remove(&id); + region.arc_map.remove(&id); + return result; + } *self = Arc::downgrade(&arc); } } diff --git a/fyrox-core/src/visitor/mod.rs b/fyrox-core/src/visitor/mod.rs index 5d496a5c3..6b614b739 100644 --- a/fyrox-core/src/visitor/mod.rs +++ b/fyrox-core/src/visitor/mod.rs @@ -155,7 +155,7 @@ where }), } } else { - Err(VisitError::FieldDoesNotExist(name.to_owned())) + Err(VisitError::field_does_not_exist(name, visitor)) } } else if visitor.find_field(name).is_some() { Err(VisitError::FieldAlreadyExists(name.to_owned())) @@ -312,13 +312,25 @@ bitflags! { /// Whether the value of `x` gets written into `visitor` or overwritten with a value from `visitor` is determined /// by whether [Visitor::is_reading()] returns true or false. pub struct Visitor { + /// The nodes that make up this visitors tree. nodes: Pool, + /// The id for the next `Rc` or `Arc` that the visitor encounters. unique_id_counter: u64, + /// The name of the type of the `Rc` or `Arc` associated with each id. + /// This allows type mismatch errors to include type information. + type_name_map: FxHashMap, + /// The `Rc` value associated with each id, to allow the visitor to find the `Rc` when reading. rc_map: FxHashMap>, + /// The `Arc` value associated with each id, to allow the visitor to find the `Arc` when reading. arc_map: FxHashMap>, + /// True if this visitor is being read from, during loading. + /// False if this visitor is being written to, during saving. reading: bool, + /// The handle of the node that is currently being written to or read from. current_node: Handle, + /// The handle of the start of the tree. root: Handle, + /// The version number of the visitor. See [`VisitorVersion`]. version: u32, /// A place to store whatever objects may be needed to help with reading and writing values. pub blackboard: Blackboard, @@ -524,6 +536,7 @@ impl Visitor { Self { nodes, unique_id_counter: 1, + type_name_map: FxHashMap::default(), rc_map: FxHashMap::default(), arc_map: FxHashMap::default(), reading: false, @@ -555,6 +568,7 @@ impl Visitor { (id, false) } else { let id = self.gen_unique_id(); + self.type_name_map.insert(id, std::any::type_name::()); self.rc_map.insert(id, rc.clone()); (id, true) } @@ -574,6 +588,7 @@ impl Visitor { (id, false) } else { let id = self.gen_unique_id(); + self.type_name_map.insert(id, std::any::type_name::()); self.arc_map.insert(id, arc.clone()); (id, true) } @@ -633,7 +648,9 @@ impl Visitor { self.current_node = region; Ok(RegionGuard(self)) } else { - Err(VisitError::RegionDoesNotExist(self.build_breadcrumb(" > "))) + Err(VisitError::RegionDoesNotExist( + self.breadcrumbs() + " > " + name, + )) } } else { // Make sure that node does not exist already. @@ -655,6 +672,15 @@ impl Visitor { } } + /// Return a string representing all the regions from the root + /// to the current node. + pub fn breadcrumbs(&self) -> String { + self.build_breadcrumb(" > ") + } + + /// Return a string representing all the regions from the root + /// to the current node, using the given string as the separator between + /// region names. fn build_breadcrumb(&self, separator: &str) -> String { let mut rev = String::new(); let mut handle = self.current_node; @@ -689,6 +715,25 @@ impl Visitor { } } + /// Get the content of the current node in human-readable form. + pub fn debug(&self) -> String { + let mut w = Cursor::new(Vec::::new()); + let result = self.debug_to(&mut w); + match result { + Ok(()) => String::from_utf8_lossy(w.get_ref()).into_owned(), + Err(err) => err.to_string(), + } + } + + /// Write the content of the current node in human-readable form. + pub fn debug_to(&self, w: &mut W) -> VisitResult { + let writer = AsciiWriter::default(); + writer.write_node(self, &self.nodes[self.current_node], 0, w)?; + writeln!(w)?; + w.flush()?; + Ok(()) + } + /// Create a string containing all the data of this Visitor in ascii form. The string is /// formatted to be human-readable with each node on its own line and tabs to indent child nodes. pub fn save_ascii_to_string(&self) -> String { diff --git a/fyrox-core/src/visitor/pod.rs b/fyrox-core/src/visitor/pod.rs index fc7856f52..85de457bf 100644 --- a/fyrox-core/src/visitor/pod.rs +++ b/fyrox-core/src/visitor/pod.rs @@ -26,6 +26,17 @@ use crate::visitor::{ Visit, VisitResult, Visitor, }; +const POD_TYPES: &[&str] = &[ + "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "f32", "f64", +]; + +fn type_id_to_str(type_id: u8) -> &'static str { + POD_TYPES + .get(type_id as usize) + .copied() + .unwrap_or("Invalid Type") +} + /// Trait for datatypes that can be converted directly into bytes. /// This is required for the type to be used in the Vec of a [PodVecView]. pub trait Pod: Copy { @@ -139,7 +150,10 @@ impl Visit for PodVecView<'_, T> { *self.vec = data; Ok(()) } else { - Err(VisitError::TypeMismatch) + Err(VisitError::TypeMismatch { + expected: type_id_to_str(self.type_id), + actual: type_id_to_str(*type_id), + }) } } _ => Err(VisitError::FieldTypeDoesNotMatch { @@ -148,7 +162,7 @@ impl Visit for PodVecView<'_, T> { }), } } else { - Err(VisitError::FieldDoesNotExist(name.to_owned())) + Err(VisitError::field_does_not_exist(name, visitor)) } } else if visitor.find_field(name).is_some() { Err(VisitError::FieldAlreadyExists(name.to_owned())) diff --git a/fyrox-core/src/visitor/reader/ascii.rs b/fyrox-core/src/visitor/reader/ascii.rs index bf5f85282..12f102496 100644 --- a/fyrox-core/src/visitor/reader/ascii.rs +++ b/fyrox-core/src/visitor/reader/ascii.rs @@ -404,6 +404,7 @@ impl Reader for AsciiReader<'_> { let mut visitor = Visitor { nodes: Pool::new(), unique_id_counter: 1, + type_name_map: Default::default(), rc_map: Default::default(), arc_map: Default::default(), reading: true, diff --git a/fyrox-core/src/visitor/reader/binary.rs b/fyrox-core/src/visitor/reader/binary.rs index ae015a3c3..ac997f45a 100644 --- a/fyrox-core/src/visitor/reader/binary.rs +++ b/fyrox-core/src/visitor/reader/binary.rs @@ -257,6 +257,7 @@ impl Reader for BinaryReader<'_> { let mut visitor = Visitor { nodes: Pool::new(), unique_id_counter: 1, + type_name_map: Default::default(), rc_map: Default::default(), arc_map: Default::default(), reading: true, diff --git a/fyrox-graph/src/lib.rs b/fyrox-graph/src/lib.rs index 634288f1d..8448f7ccf 100644 --- a/fyrox-graph/src/lib.rs +++ b/fyrox-graph/src/lib.rs @@ -686,11 +686,17 @@ pub trait AbstractSceneGraph: 'static { ) -> Option<&mut dyn AbstractSceneNode>; } +/// BaseSceneGraph is a dyn-compatible trait for all scene graphs to implement. +/// Methods that would not be dyn-compatible are available through the +/// [`SceneGraph`] trait. pub trait BaseSceneGraph: AbstractSceneGraph { type Prefab: PrefabData; type NodeContainer: PayloadContainer; type Node: SceneGraphNode; + /// Generate a string that briefly summarizes the content of the graph for debugging. + fn summary(&self) -> String; + /// Returns actual type id of the node. fn actual_type_id(&self, handle: Handle) -> Option; @@ -832,6 +838,9 @@ pub trait BaseSceneGraph: AbstractSceneGraph { } } +/// SceneGraph is a non-dyn-compatible trait for all scene graphs to implement. +/// To use a scene graph as a dyn object, use `dyn BaseSceneGraph` because +/// [`BaseSceneGraph`] is dyn-compatible. pub trait SceneGraph: BaseSceneGraph { type ObjectType: Sized; /// Creates new iterator that iterates over internal collection giving (handle; node) pairs. @@ -1097,6 +1106,7 @@ pub trait SceneGraph: BaseSceneGraph { &self, from: Handle, ) -> impl Iterator, &Self::Node)> { + self.try_get_node(from).expect("Handle must be valid!"); GraphTraverseIterator { graph: self, stack: vec![from], @@ -1148,7 +1158,6 @@ pub trait SceneGraph: BaseSceneGraph { let mut nodes_to_delete = Vec::new(); for (_, node) in self.traverse_iter(instance_root) { if let Some(resource) = node.resource() { - let kind = resource.kind(); if let Some(model) = resource.state().data() { if !model .graph() @@ -1158,21 +1167,19 @@ pub trait SceneGraph: BaseSceneGraph { Log::warn(format!( "Node {} ({}:{}) and its children will be deleted, because it \ - does not exist in the parent asset `{}`!", + does not exist in the parent asset!", node.name(), node.self_handle().index(), node.self_handle().generation(), - kind )) } } else { Log::warn(format!( "Node {} ({}:{}) and its children will be deleted, because its \ - parent asset `{}` failed to load!", + parent asset failed to load!", node.name(), node.self_handle().index(), node.self_handle().generation(), - kind )) } } @@ -1806,6 +1813,10 @@ mod test { type NodeContainer = NodeContainer; type Node = Node; + fn summary(&self) -> String { + "Summary".to_string() + } + fn root(&self) -> Handle { self.root } diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index ececa3c80..716d20b1c 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -1059,8 +1059,8 @@ impl ResourceGraphVertex { pub fn resolve(&self) { Log::info(format!( - "Resolving {} resource from dependency graph...", - self.resource.kind() + "Resolving {:?} resource from dependency graph...", + self.resource.resource_uuid() )); // Wait until resource is fully loaded, then resolve. @@ -2416,8 +2416,8 @@ impl Engine { if let ResourceEvent::Reloaded(resource) = event { if let Some(model) = resource.try_cast::() { Log::info(format!( - "A model resource {} was reloaded, propagating changes...", - model.kind() + "A model resource {:?} was reloaded, propagating changes...", + model.resource_uuid() )); // Build resource dependency graph and resolve it first. @@ -2799,7 +2799,10 @@ impl Engine { // Deserialize prefab scene content. for (model, scene_state) in prefab_scenes { - Log::info(format!("Deserializing {} prefab content...", model.kind())); + Log::info(format!( + "Deserializing {:?} prefab content...", + model.resource_uuid() + )); scene_state.deserialize_into_prefab_scene( &model, diff --git a/fyrox-impl/src/renderer/cache/texture.rs b/fyrox-impl/src/renderer/cache/texture.rs index 1901e9cc8..c893acfb6 100644 --- a/fyrox-impl/src/renderer/cache/texture.rs +++ b/fyrox-impl/src/renderer/cache/texture.rs @@ -255,8 +255,7 @@ impl TextureCache { drop(texture_data_guard); err_once!( texture_resource.key() as usize, - "Failed to create GPU texture from {} texture. Reason: {:?}", - texture_resource.kind(), + "Failed to create GPU texture from texture. Reason: {:?}", e, ); } diff --git a/fyrox-impl/src/resource/gltf/material.rs b/fyrox-impl/src/resource/gltf/material.rs index 6d4e71324..795539cbe 100644 --- a/fyrox-impl/src/resource/gltf/material.rs +++ b/fyrox-impl/src/resource/gltf/material.rs @@ -18,7 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -use std::path::{Path, PathBuf}; +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; use crate::{ asset::{manager::ResourceManager, state::LoadError, untyped::ResourceKind, Resource}, @@ -98,6 +101,24 @@ pub enum GltfMaterialError { Texture(TextureError), } +impl std::error::Error for GltfMaterialError {} + +impl Display for GltfMaterialError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GltfMaterialError::ShaderLoadFailed => f.write_str("Shader load failed"), + GltfMaterialError::InvalidIndex => f.write_str("Invalid material index"), + GltfMaterialError::UnsupportedURI(uri) => { + write!(f, "Unsupported material URI {uri:?}") + } + GltfMaterialError::TextureNotFound(uri) => write!(f, "Texture not found: {uri:?}"), + GltfMaterialError::Load(error) => Display::fmt(error, f), + GltfMaterialError::Base64(error) => Display::fmt(error, f), + GltfMaterialError::Texture(error) => Display::fmt(error, f), + } + } +} + impl From for GltfMaterialError { fn from(error: LoadError) -> Self { GltfMaterialError::Load(error) diff --git a/fyrox-impl/src/resource/gltf/mod.rs b/fyrox-impl/src/resource/gltf/mod.rs index 70b788de3..e9e1210c4 100644 --- a/fyrox-impl/src/resource/gltf/mod.rs +++ b/fyrox-impl/src/resource/gltf/mod.rs @@ -47,6 +47,7 @@ use crate::scene::Scene; use gltf::json; use gltf::Document; use gltf::Gltf; +use std::fmt::Display; use std::path::{Path, PathBuf}; use std::sync::Arc; use uuid::Uuid; @@ -87,6 +88,27 @@ enum GltfLoadError { JSON(json::Error), } +impl std::error::Error for GltfLoadError {} + +impl Display for GltfLoadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GltfLoadError::InvalidIndex => f.write_str("Invalid index"), + GltfLoadError::InvalidPath => f.write_str("Invalid path"), + GltfLoadError::UnsupportedURI(uri) => write!(f, "Unsupported URL {uri:?}"), + GltfLoadError::MissingEmbeddedBin => f.write_str("Missing embedded bin"), + GltfLoadError::Gltf(error) => Display::fmt(error, f), + GltfLoadError::Texture(error) => Display::fmt(error, f), + GltfLoadError::File(error) => Display::fmt(error, f), + GltfLoadError::Base64(error) => Display::fmt(error, f), + GltfLoadError::Load(error) => Display::fmt(error, f), + GltfLoadError::Material(error) => Display::fmt(error, f), + GltfLoadError::Surface(error) => Display::fmt(error, f), + GltfLoadError::JSON(error) => Display::fmt(error, f), + } + } +} + impl From for GltfLoadError { fn from(error: json::Error) -> Self { GltfLoadError::JSON(error) diff --git a/fyrox-impl/src/resource/gltf/surface.rs b/fyrox-impl/src/resource/gltf/surface.rs index 05be4e82e..283f85c14 100644 --- a/fyrox-impl/src/resource/gltf/surface.rs +++ b/fyrox-impl/src/resource/gltf/surface.rs @@ -41,6 +41,7 @@ use crate::{ scene::mesh::buffer::{VertexAttributeUsage, VertexReadTrait}, }; +use std::fmt::Display; use std::num::TryFromIntError; /// This type represents any error that may occur while importing mesh data from glTF. @@ -83,6 +84,30 @@ pub enum SurfaceDataError { Fetch(buffer::VertexFetchError), } +impl std::error::Error for SurfaceDataError {} + +impl Display for SurfaceDataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SurfaceDataError::CountMismatch => f.write_str("Count mismatch"), + SurfaceDataError::MissingPosition => f.write_str("Missing position"), + SurfaceDataError::MissingNormal => f.write_str("Missing normal"), + SurfaceDataError::MissingTexCoords => f.write_str("Missing texcoords"), + SurfaceDataError::MissingBoneWeight => f.write_str("Missing bone weight"), + SurfaceDataError::MissingBoneIndex => f.write_str("Missing bone index"), + SurfaceDataError::InvalidBoneIndex => f.write_str("Invalid bone index"), + SurfaceDataError::InvalidMode => f.write_str("Invalid mode"), + SurfaceDataError::InvalidIndex => f.write_str("Invalid index"), + SurfaceDataError::Int(error) => Display::fmt(error, f), + SurfaceDataError::InvalidVertexCount(geometry_type, count) => { + write!(f, "Cannot have {count} vertices {geometry_type}.") + } + SurfaceDataError::Validation(error) => Display::fmt(error, f), + SurfaceDataError::Fetch(error) => Display::fmt(error, f), + } + } +} + impl From for SurfaceDataError { fn from(error: ValidationError) -> Self { SurfaceDataError::Validation(error) @@ -108,6 +133,16 @@ pub enum GeometryType { TriangleFan, } +impl Display for GeometryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GeometryType::Triangles => f.write_str("triangles"), + GeometryType::TriangleStrip => f.write_str("triangle strip"), + GeometryType::TriangleFan => f.write_str("triangle fan"), + } + } +} + #[derive(Debug, Default, Clone)] pub struct BlendShapeInfo { pub name: String, diff --git a/fyrox-impl/src/resource/model/mod.rs b/fyrox-impl/src/resource/model/mod.rs index bcd2282fc..e92523c94 100644 --- a/fyrox-impl/src/resource/model/mod.rs +++ b/fyrox-impl/src/resource/model/mod.rs @@ -491,6 +491,12 @@ impl ModelResourceExtension for ModelResource { } fn begin_instantiation<'a>(&'a self, dest_scene: &'a mut Scene) -> InstantiationContext<'a> { + if !self.is_ok() { + Log::err(format!( + "Instantiating a model from a resource that is not loaded: {self:?}" + )); + panic!(); + } InstantiationContext { model: self, dest_scene, diff --git a/fyrox-impl/src/scene/camera.rs b/fyrox-impl/src/scene/camera.rs index dfa73eb75..0c53419bd 100644 --- a/fyrox-impl/src/scene/camera.rs +++ b/fyrox-impl/src/scene/camera.rs @@ -860,7 +860,7 @@ impl Display for ColorGradingLutCreationError { ) } ColorGradingLutCreationError::Texture(v) => { - write!(f, "Texture load error: {v:?}") + write!(f, "Texture load error: {v}") } } } diff --git a/fyrox-impl/src/scene/graph/mod.rs b/fyrox-impl/src/scene/graph/mod.rs index cdb2ec565..6e5c62e69 100644 --- a/fyrox-impl/src/scene/graph/mod.rs +++ b/fyrox-impl/src/scene/graph/mod.rs @@ -78,6 +78,7 @@ use bitflags::bitflags; use fxhash::{FxHashMap, FxHashSet}; use fyrox_core::pool::ObjectOrVariant; use fyrox_graph::SceneGraphNode; +use std::fmt::Write; use std::ops::Deref; use std::{ any::{Any, TypeId}, @@ -127,7 +128,7 @@ impl GraphPerformanceStatistics { pub type NodePool = Pool; /// See module docs. -#[derive(Debug, Reflect)] +#[derive(Reflect)] pub struct Graph { #[reflect(hidden)] root: Handle, @@ -171,6 +172,22 @@ pub struct Graph { instance_id_map: FxHashMap>, } +impl Debug for Graph { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Graph") + .field("physics", &self.physics) + .field("physics2d", &self.physics2d) + .field("sound_context", &self.sound_context) + .field("performance_statistics", &self.performance_statistics) + .field("event_broadcaster", &self.event_broadcaster) + .field("lightmap", &self.lightmap) + .field("instance_id_map", &self.instance_id_map) + .finish()?; + f.write_char('\n')?; + f.write_str(&self.summary()) + } +} + impl Clone for Graph { fn clone(&self) -> Self { self.clone_ex( @@ -268,7 +285,7 @@ fn clear_links(mut node: Node) -> Node { } /// A set of switches that allows you to disable a particular step of graph update pipeline. -#[derive(Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GraphUpdateSwitches { /// Enables the physics update to have a non-zero `dt`. pub physics_dt: bool, @@ -353,6 +370,30 @@ impl Graph { graph } + fn recursive_summary(&self, indent: usize, current: Handle, result: &mut String) { + for _ in 0..indent { + result.push_str(" "); + } + let Some(node) = self.try_get(current) else { + use std::fmt::Write; + writeln!(result, "{}: Failed to get", current).unwrap(); + return; + }; + result.push_str(&node.summary()); + result.push('\n'); + for script in node.scripts() { + for _ in 0..indent + 1 { + result.push_str(" "); + } + result.push_str("+ Script: "); + result.push_str(&script.summary()); + result.push('\n'); + } + for child in node.children() { + self.recursive_summary(indent + 1, *child, result); + } + } + /// Sets new root of the graph and attaches the old root to the new root. Old root becomes a child /// node of the new root. pub fn change_root_node(&mut self, root: Node) { @@ -1683,6 +1724,13 @@ impl BaseSceneGraph for Graph { type NodeContainer = NodeContainer; type Node = Node; + /// Create a brief debug summary of the contents of this graph. + fn summary(&self) -> String { + let mut result = String::new(); + self.recursive_summary(0, self.root, &mut result); + result + } + #[inline] fn actual_type_id(&self, handle: Handle) -> Option { self.pool @@ -2192,21 +2240,21 @@ mod test { let mesh_pivot = derived_scene .graph .find_by_name_from_root("MeshPivot") - .unwrap() + .expect("Missing MeshPivot") .0; let mesh = derived_scene .graph .find_by_name(mesh_pivot, "Mesh") - .unwrap() + .expect("Missing Mesh") .0; derived_scene .graph .find_by_name_from_root("AddedLater") - .unwrap(); + .expect("Missing AddedLater"); derived_scene .graph .find_by_name(mesh, "NewChildOfMesh") - .unwrap(); + .expect("Missing NewChildOfMesh"); } } diff --git a/fyrox-impl/src/scene/graph/physics.rs b/fyrox-impl/src/scene/graph/physics.rs index 17df65fc5..ae859622a 100644 --- a/fyrox-impl/src/scene/graph/physics.rs +++ b/fyrox-impl/src/scene/graph/physics.rs @@ -517,6 +517,9 @@ fn make_trimesh( for surface in mesh.surfaces() { let shared_data = surface.data(); + if !shared_data.is_ok() { + continue; + } let shared_data = shared_data.data_ref(); let vertices = &shared_data.vertex_buffer; diff --git a/fyrox-impl/src/scene/mesh/mod.rs b/fyrox-impl/src/scene/mesh/mod.rs index fb2ec5146..3c7831561 100644 --- a/fyrox-impl/src/scene/mesh/mod.rs +++ b/fyrox-impl/src/scene/mesh/mod.rs @@ -639,8 +639,10 @@ impl NodeTrait for Mesh { } else { for surface in self.surfaces.iter() { let data = surface.data(); - let data = data.data_ref(); - extend_aabb_from_vertex_buffer(&data.vertex_buffer, &mut bounding_box); + if data.is_ok() { + let data = data.data_ref(); + extend_aabb_from_vertex_buffer(&data.vertex_buffer, &mut bounding_box); + } } } @@ -721,6 +723,9 @@ impl NodeTrait for Mesh { RdcControlFlow::Break } else { for surface in self.surfaces().iter() { + if !surface.data_ref().is_ok() { + continue; + } let is_skinned = !surface.bones.is_empty(); let world = if is_skinned { diff --git a/fyrox-impl/src/scene/mesh/surface.rs b/fyrox-impl/src/scene/mesh/surface.rs index ec2e27adc..eb0335ff0 100644 --- a/fyrox-impl/src/scene/mesh/surface.rs +++ b/fyrox-impl/src/scene/mesh/surface.rs @@ -336,7 +336,7 @@ impl SurfaceData { if v1 == v2 || v1 == v3 || v2 == v3 { Log::warn(format!( "Degenerated triangle found when calculating tangents. Lighting may be \ - incorrect! Triangle indices: {triangle:?}. Triangle vertices: {v1} {v2} {v3}", + incorrect! Triangle indices: {triangle:?}. Triangle vertices: {v1:?} {v2:?} {v3:?}", )); } diff --git a/fyrox-impl/src/scene/mod.rs b/fyrox-impl/src/scene/mod.rs index 85892d820..a6d407352 100644 --- a/fyrox-impl/src/scene/mod.rs +++ b/fyrox-impl/src/scene/mod.rs @@ -435,7 +435,13 @@ impl SceneLoader { )); // Wait everything. - join_all(used_resources.into_iter()).await; + let results = join_all(used_resources.into_iter()).await; + + for result in results { + if let Err(err) = result { + Log::err(format!("Scene resource loading error: {:?}", err)); + } + } Log::info(format!( "SceneLoader::finish() - All {used_resources_count} resources have finished loading." diff --git a/fyrox-impl/src/scene/node/mod.rs b/fyrox-impl/src/scene/node/mod.rs index 9dac8f6eb..559fd51bc 100644 --- a/fyrox-impl/src/scene/node/mod.rs +++ b/fyrox-impl/src/scene/node/mod.rs @@ -134,6 +134,36 @@ pub enum RdcControlFlow { /// A main trait for any scene graph node. pub trait NodeTrait: BaseNodeTrait + Reflect + Visit + ComponentProvider { + /// Brief debugging information about this node. + fn summary(&self) -> String { + use std::fmt::Write; + let mut result = String::new(); + let type_name = self + .type_name() + .strip_prefix("fyrox_impl::scene::") + .unwrap_or(self.type_name()); + write!(result, "{} {}<{}>", self.handle(), self.name(), type_name,).unwrap(); + if self.children().len() == 1 { + result.push_str(" 1 child"); + } else if self.children().len() > 1 { + write!(result, " {} children", self.children().len()).unwrap(); + } + if self.script_count() > 0 { + write!(result, " {} scripts", self.script_count()).unwrap(); + } + if self.is_resource_instance_root() { + result.push_str(" root"); + } + let origin = self.original_handle_in_resource(); + if origin.is_some() { + write!(result, " from:{}", origin).unwrap(); + } + if let Some(r) = self.resource() { + write!(result, " {}", r.summary()).unwrap(); + } + result + } + /// Returns axis-aligned bounding box in **local space** of the node. fn local_bounding_box(&self) -> AxisAlignedBoundingBox; @@ -807,6 +837,7 @@ mod test { let derived_data = derived_asset.data_ref(); let derived_scene = derived_data.get_scene(); + let pivot = derived_scene .graph .find_by_name_from_root("Pivot") diff --git a/fyrox-impl/src/script/mod.rs b/fyrox-impl/src/script/mod.rs index 91684ebad..8cc45ca66 100644 --- a/fyrox-impl/src/script/mod.rs +++ b/fyrox-impl/src/script/mod.rs @@ -859,6 +859,20 @@ impl Script { } } + /// Generate a brief summary of this script for debugging purposes. + pub fn summary(&self) -> String { + let mut summary = String::new(); + if self.initialized { + summary.push_str("init "); + } + if self.started { + summary.push_str("start "); + } + use std::fmt::Write; + write!(summary, "{:?}", self.instance).unwrap(); + summary + } + /// Performs downcasting to a particular type. #[inline] pub fn cast(&self) -> Option<&T> { diff --git a/fyrox-resource/src/lib.rs b/fyrox-resource/src/lib.rs index 520378fe8..a1c3aee7e 100644 --- a/fyrox-resource/src/lib.rs +++ b/fyrox-resource/src/lib.rs @@ -39,8 +39,8 @@ use crate::{ use fxhash::FxHashSet; pub use fyrox_core as core; use fyrox_core::{combine_uuids, log::Log, SafeLock}; -use std::any::Any; use std::path::PathBuf; +use std::{any::Any, fmt::Display}; use std::{ error::Error, fmt::{Debug, Formatter}, @@ -110,9 +110,9 @@ pub trait TypedResourceData: ResourceData + Default + TypeUuidProvider {} impl TypedResourceData for T where T: ResourceData + Default + TypeUuidProvider {} /// A trait for resource load error. -pub trait ResourceLoadError: 'static + Debug + Send + Sync {} +pub trait ResourceLoadError: 'static + Debug + Display + Send + Sync {} -impl ResourceLoadError for T where T: 'static + Debug + Send + Sync {} +impl ResourceLoadError for T where T: 'static + Debug + Display + Send + Sync {} /// Provides typed access to a resource state. pub struct ResourceHeaderGuard<'a, T> @@ -178,13 +178,24 @@ where /// ## Default State /// /// Default state of the resource will be [`ResourceState::Ok`] with `T::default`. -#[derive(Debug, Reflect)] +#[derive(Reflect)] pub struct Resource { untyped: UntypedResource, #[reflect(hidden)] phantom: PhantomData, } +impl Debug for Resource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Resource<{}>({})", + std::any::type_name::(), + self.untyped + ) + } +} + impl AsRef for Resource { fn as_ref(&self) -> &UntypedResource { &self.untyped @@ -276,6 +287,10 @@ impl Resource where T: TypedResourceData, { + /// Create a summary of this resource handle, wht UUID, embedded/external, and Ok, Pending, Error, etc. + pub fn summary(&self) -> String { + format!("{}", self.untyped) + } /// Creates new resource in pending state. #[inline] pub fn new_pending(path: PathBuf, kind: ResourceKind) -> Self { @@ -523,15 +538,13 @@ where ResourceState::Pending { .. } => { write!( f, - "Attempt to get reference to resource data while it is not loaded! Path is {}", - self.guard.kind + "Attempt to get reference to resource data while it is loading!" ) } ResourceState::LoadError { .. } => { write!( f, - "Attempt to get reference to resource data which failed to load! Path is {}", - self.guard.kind + "Attempt to get reference to resource data which failed to load!" ) } ResourceState::Ok { ref data, .. } => data.fmt(f), @@ -549,15 +562,20 @@ where match self.guard.state { ResourceState::Pending { .. } => { panic!( - "Attempt to get reference to resource data while it is not loaded! Path is {}", - self.guard.kind + "Attempt to get reference to resource data while it is loading! Type {}", + std::any::type_name::() ) } - ResourceState::LoadError { .. } => { - panic!( - "Attempt to get reference to resource data which failed to load! Path is {}", - self.guard.kind - ) + ResourceState::LoadError { + ref path, + ref error, + } => { + let path = if path.as_os_str().is_empty() { + "Unknown".to_string() + } else { + format!("{path:?}") + }; + panic!("Attempt to get reference to resource data which failed to load! Type {}. Path: {path}. Error: {error:?}", std::any::type_name::()) } ResourceState::Ok { ref data, .. } => (&**data as &dyn Any) .downcast_ref() @@ -574,16 +592,10 @@ where let header = &mut *self.guard; match header.state { ResourceState::Pending { .. } => { - panic!( - "Attempt to get reference to resource data while it is not loaded! Path is {}", - header.kind - ) + panic!("Attempt to get reference to resource data while it is loading!") } ResourceState::LoadError { .. } => { - panic!( - "Attempt to get reference to resource data which failed to load! Path is {}", - header.kind - ) + panic!("Attempt to get reference to resource data which failed to load!") } ResourceState::Ok { ref mut data, .. } => (&mut **data as &mut dyn Any) .downcast_mut() diff --git a/fyrox-resource/src/state.rs b/fyrox-resource/src/state.rs index c152ea6cb..a97dfbaa9 100644 --- a/fyrox-resource/src/state.rs +++ b/fyrox-resource/src/state.rs @@ -29,6 +29,7 @@ use crate::{ use fyrox_core::reflect::ReflectHandle; use fyrox_core::warn; use std::any::{Any, TypeId}; +use std::fmt::Display; use std::path::PathBuf; use std::{ ops::{Deref, DerefMut}, @@ -65,11 +66,13 @@ impl DerefMut for WakersList { } } -/// Arbitrary loading error, that could be optionally be empty. +/// Arbitrary loading error, that could be optionally be empty. #[derive(Reflect, Debug, Clone, Default)] #[reflect(hide_all)] pub struct LoadError(pub Option>); +impl std::error::Error for LoadError {} + impl LoadError { /// Creates new loading error from a value of the given type. pub fn new(value: T) -> Self { @@ -77,6 +80,15 @@ impl LoadError { } } +impl Display for LoadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + None => f.write_str("None"), + Some(x) => Display::fmt(x, f), + } + } +} + #[doc(hidden)] #[derive(Debug)] pub struct ResourceDataWrapper(pub Box); diff --git a/fyrox-resource/src/untyped.rs b/fyrox-resource/src/untyped.rs index 2496d1762..9711e4c5d 100644 --- a/fyrox-resource/src/untyped.rs +++ b/fyrox-resource/src/untyped.rs @@ -33,6 +33,7 @@ use crate::{ MODEL_RESOURCE_UUID, SHADER_RESOURCE_UUID, SOUND_BUFFER_RESOURCE_UUID, TEXTURE_RESOURCE_UUID, }; use fyrox_core::err; +use std::fmt::Write; use std::{ error::Error, ffi::OsStr, @@ -166,6 +167,21 @@ pub struct ResourceHeader { old_format_path: Option, } +impl Display for ResourceHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.kind { + ResourceKind::Embedded => f.write_str("Embed")?, + ResourceKind::External => f.write_str("Extern")?, + } + f.write_char(':')?; + match &self.state { + ResourceState::Pending { .. } => f.write_str("Pending"), + ResourceState::LoadError { path, error } => write!(f, "Error({path:?}, {error})"), + ResourceState::Ok { .. } => f.write_str("Ok"), + } + } +} + impl Visit for ResourceHeader { fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { let mut region = visitor.enter_region(name)?; @@ -390,6 +406,16 @@ impl Default for UntypedResource { } } +impl Display for UntypedResource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(header) = self.0.try_lock() { + Display::fmt(&header, f) + } else { + f.write_str("locked") + } + } +} + impl Debug for UntypedResource { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Resource") diff --git a/fyrox-sound/src/renderer/hrtf.rs b/fyrox-sound/src/renderer/hrtf.rs index b769d97f5..b044119e5 100644 --- a/fyrox-sound/src/renderer/hrtf.rs +++ b/fyrox-sound/src/renderer/hrtf.rs @@ -94,9 +94,50 @@ use fyrox_resource::{ Resource, ResourceData, }; use hrtf::HrirSphere; -use std::error::Error; -use std::path::Path; +use std::{error::Error, ops::Deref}; use std::{fmt::Debug, fmt::Formatter, path::PathBuf, sync::Arc}; +use std::{fmt::Display, path::Path}; + +/// An error that occurs during HRIR sphere loading. +pub struct HrtfError(pub hrtf::HrtfError); + +impl std::error::Error for HrtfError {} + +impl From for HrtfError { + fn from(value: hrtf::HrtfError) -> Self { + Self(value) + } +} + +impl From for hrtf::HrtfError { + fn from(value: HrtfError) -> Self { + value.0 + } +} + +impl Deref for HrtfError { + type Target = hrtf::HrtfError; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for HrtfError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for HrtfError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.0 { + hrtf::HrtfError::IoError(error) => Display::fmt(error, f), + hrtf::HrtfError::InvalidFileFormat => f.write_str("Invalid file format"), + hrtf::HrtfError::InvalidLength(n) => write!(f, "Invalid length {n}"), + } + } +} /// See module docs. #[derive(Clone, Debug, Default, Reflect)] @@ -255,8 +296,9 @@ impl ResourceLoader for HrirSphereLoader { fn load(&self, path: PathBuf, io: Arc) -> BoxedLoaderFuture { Box::pin(async move { let reader = io.file_reader(&path).await.map_err(LoadError::new)?; - let hrir_sphere = - HrirSphere::new(reader, context::SAMPLE_RATE).map_err(LoadError::new)?; + let hrir_sphere = HrirSphere::new(reader, context::SAMPLE_RATE) + .map_err(HrtfError::from) + .map_err(LoadError::new)?; Ok(LoaderPayload::new(HrirSphereResourceData { hrir_sphere: Some(hrir_sphere), })) diff --git a/fyrox-sound/src/source.rs b/fyrox-sound/src/source.rs index 9722be4c7..eeb347dae 100644 --- a/fyrox-sound/src/source.rs +++ b/fyrox-sound/src/source.rs @@ -242,6 +242,9 @@ impl SoundSource { match buffer.state().data() { None => return Err(SoundError::BufferFailedToLoad), Some(locked_buffer) => { + if locked_buffer.duration() == Duration::ZERO { + panic!("Zero duration buffer: {:?}", locked_buffer); + } // Check new buffer if streaming - it must not be used by anyone else. if let SoundBuffer::Streaming(ref mut streaming) = *locked_buffer { if streaming.use_count != 0 { diff --git a/fyrox-ui/src/control.rs b/fyrox-ui/src/control.rs index 09ca3f460..007c72bae 100644 --- a/fyrox-ui/src/control.rs +++ b/fyrox-ui/src/control.rs @@ -80,6 +80,22 @@ where pub trait Control: BaseControl + Deref + DerefMut + Reflect + Visit + ComponentProvider { + /// Brief debugging information about this node. + fn summary(&self) -> String { + use std::fmt::Write; + let mut result = String::new(); + let type_name = BaseControl::type_name(self) + .strip_prefix("fyrox_ui::") + .unwrap_or(BaseControl::type_name(self)); + write!(result, "{} {}<{}>", self.handle(), self.name(), type_name,).unwrap(); + if self.children().len() == 1 { + result.push_str(" 1 child"); + } else if self.children().len() > 1 { + write!(result, " {} children", self.children().len()).unwrap(); + } + result + } + /// This method will be called before the widget is destroyed (dropped). At the moment, when this /// method is called, the widget is still in the widget graph and can be accessed via handles. It /// is guaranteed to be called once, and only if the widget is deleted via [`crate::widget::WidgetMessage::remove`]. diff --git a/fyrox-ui/src/lib.rs b/fyrox-ui/src/lib.rs index c50d6d740..5bb53f73c 100644 --- a/fyrox-ui/src/lib.rs +++ b/fyrox-ui/src/lib.rs @@ -327,7 +327,7 @@ use std::{ cell::{Ref, RefCell, RefMut}, collections::{btree_set::BTreeSet, hash_map::Entry, VecDeque}, error::Error, - fmt::{Debug, Formatter}, + fmt::{Debug, Formatter, Write}, ops::{Deref, DerefMut, Index, IndexMut}, path::Path, sync::{ @@ -686,7 +686,7 @@ pub enum RenderMode { OnChanges, } -#[derive(Reflect, Debug)] +#[derive(Reflect)] pub struct UserInterface { screen_size: Vector2, nodes: WidgetPool, @@ -741,6 +741,48 @@ pub struct UserInterface { pub need_render: bool, } +impl Debug for UserInterface { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UserInterface") + .field("screen_size", &self.screen_size) + .field("drawing_context", &self.drawing_context) + .field("visual_debug", &self.visual_debug) + .field("root_canvas", &self.root_canvas) + .field("picked_node", &self.picked_node) + .field("prev_picked_node", &self.prev_picked_node) + .field("captured_node", &self.captured_node) + .field("keyboard_focus_node", &self.keyboard_focus_node) + .field("cursor_position", &self.cursor_position) + .field("style", &self.style) + .field("receiver", &self.receiver) + .field("sender", &self.sender) + .field("stack", &self.stack) + .field("picking_stack", &self.picking_stack) + .field("bubble_queue", &self.bubble_queue) + .field("drag_context", &self.drag_context) + .field("mouse_state", &self.mouse_state) + .field("keyboard_modifiers", &self.keyboard_modifiers) + .field("cursor_icon", &self.cursor_icon) + .field("active_tooltip", &self.active_tooltip) + .field("methods_registry", &self.methods_registry) + .field("clipboard", &self.clipboard) + .field("layout_events_receiver", &self.layout_events_receiver) + .field("layout_events_sender", &self.layout_events_sender) + .field("z_index_update_set", &self.z_index_update_set) + .field("default_font", &self.default_font) + .field("double_click_entries", &self.double_click_entries) + .field("double_click_time_slice", &self.double_click_time_slice) + .field("tooltip_appear_delay", &self.tooltip_appear_delay) + .field("standard_material", &self.standard_material) + .field("render_target", &self.render_target) + .field("render_mode", &self.render_mode) + .field("need_render", &self.need_render) + .finish()?; + f.write_char('\n')?; + f.write_str(&self.summary()) + } +} + impl Visit for UserInterface { fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { let mut region = visitor.enter_region(name)?; @@ -1155,6 +1197,22 @@ impl UserInterface { ui } + fn recursive_summary(&self, indent: usize, current: Handle, result: &mut String) { + for _ in 0..indent { + result.push_str(" "); + } + let Some(node) = self.try_get(current) else { + use std::fmt::Write; + writeln!(result, "{}: Failed to get", current).unwrap(); + return; + }; + result.push_str(&node.summary()); + result.push('\n'); + for child in node.children() { + self.recursive_summary(indent + 1, *child, result); + } + } + pub fn set_tooltip_appear_delay(&mut self, appear_delay: f32) { self.tooltip_appear_delay = appear_delay; } @@ -3275,6 +3333,12 @@ impl BaseSceneGraph for UserInterface { type NodeContainer = WidgetContainer; type Node = UiNode; + fn summary(&self) -> String { + let mut result = String::new(); + self.recursive_summary(0, self.root_canvas, &mut result); + result + } + #[inline] fn actual_type_id(&self, handle: Handle) -> Option { self.nodes