9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1007 lines
37 KiB
Rust
1007 lines
37 KiB
Rust
//! Reflection / type registry — generic, name-keyed access to components.
|
|
//!
|
|
//! The engine's *dual-editable types* principle says every component must be
|
|
//! readable and writable from the editor, from scripts, and from external tools
|
|
//! through **one** representation — without each of those callers knowing the
|
|
//! concrete Rust type. The [`TypeRegistry`] is that bridge.
|
|
//!
|
|
//! A type is registered **once** under a stable name:
|
|
//!
|
|
//! ```
|
|
//! use oxide_engine::reflect::TypeRegistry;
|
|
//! use oxide_engine::prelude::*;
|
|
//!
|
|
//! let mut registry = TypeRegistry::new();
|
|
//! registry.register::<Transform>("Transform");
|
|
//! registry.register::<Node>("Node");
|
|
//! ```
|
|
//!
|
|
//! From then on, any caller holding only the *name* can round-trip the component
|
|
//! on an entity as RON text — which is all a generic inspector or a script needs:
|
|
//!
|
|
//! ```
|
|
//! # use oxide_engine::reflect::TypeRegistry;
|
|
//! # use oxide_engine::prelude::*;
|
|
//! # let mut registry = TypeRegistry::new();
|
|
//! # registry.register::<Transform>("Transform");
|
|
//! let mut scene = Scene::new();
|
|
//! let e = scene.spawn("thing", Transform::IDENTITY);
|
|
//!
|
|
//! // Read it generically...
|
|
//! let ron = registry.get_ron(scene.world(), e, "Transform").unwrap();
|
|
//! // ...and write it back generically, no concrete type at the call site.
|
|
//! registry.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap();
|
|
//! ```
|
|
//!
|
|
//! That path is **whole-value** reflection (the unit is one component,
|
|
//! serialized). On top of it, [`register_reflected`](TypeRegistry::register_reflected)
|
|
//! adds **per-field** reflection — named fields each addressable on their own —
|
|
//! for types that derive [`Reflect`], which is what a Unity/Godot-style
|
|
//! inspector needs to render one widget per field. Both coexist: the registry
|
|
//! addresses *types* by name, while [`Reflect`] addresses *fields* within a
|
|
//! value.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use hecs::{Component, Entity, World};
|
|
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
|
|
/// `#[derive(Reflect)]` — generates the [`Reflect`] impl for a struct's public
|
|
/// fields. Shares its name with the [`Reflect`] trait (macro vs. type
|
|
/// namespace), exactly like `serde`'s `Serialize`.
|
|
pub use oxide_engine_derive::Reflect;
|
|
|
|
/// `#[derive(ReflectEnum)]` — generates the [`ReflectEnum`] impl for a fieldless
|
|
/// enum, exposing its variant names for inspector dropdowns.
|
|
pub use oxide_engine_derive::ReflectEnum;
|
|
|
|
/// Errors from generic, name-keyed component access.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ReflectError {
|
|
/// No type was registered under this name.
|
|
#[error("no registered type named '{0}'")]
|
|
UnknownType(String),
|
|
|
|
/// The entity is not live in the world.
|
|
#[error("entity is not live in this world")]
|
|
NoSuchEntity,
|
|
|
|
/// The entity is live but does not carry this component.
|
|
#[error("entity has no component '{0}'")]
|
|
Missing(String),
|
|
|
|
/// The RON text could not be parsed into the named type.
|
|
#[error("failed to parse '{type_name}': {message}")]
|
|
Parse {
|
|
/// The registered name being parsed.
|
|
type_name: String,
|
|
/// The underlying parser message.
|
|
message: String,
|
|
},
|
|
|
|
/// A per-field operation named a field this type does not reflect.
|
|
#[error("no reflected field named '{0}'")]
|
|
UnknownField(String),
|
|
|
|
/// A per-field operation targeted a type registered for whole-value access
|
|
/// only (registered with `register`, not `register_reflected`).
|
|
#[error("type '{0}' is not field-reflected")]
|
|
NotReflected(String),
|
|
|
|
/// The RON text could not be parsed into a single field's type.
|
|
#[error("failed to parse field '{field}': {message}")]
|
|
FieldParse {
|
|
/// The field being parsed.
|
|
field: String,
|
|
/// The underlying parser message.
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
/// Per-field reflection generated by `#[derive(Reflect)]`.
|
|
///
|
|
/// Whole-value reflection ([`TypeRegistry::get_ron`] / [`set_ron`]) is enough
|
|
/// for serialization and scripts, but a Unity/Godot-style inspector needs to
|
|
/// see *named fields* so it can render one widget per field. `Reflect`
|
|
/// provides exactly that, without exposing the concrete field types to the
|
|
/// caller: each field is addressed by name and round-trips as RON (the same
|
|
/// representation the whole-value path uses).
|
|
///
|
|
/// Implement it with the derive — see
|
|
/// [`oxide_engine_derive::Reflect`](Reflect) (re-exported here as the
|
|
/// derive macro of the same name). Only **public** fields are reflected;
|
|
/// annotate a public field with `#[reflect(skip)]` to exclude it.
|
|
///
|
|
/// [`set_ron`]: TypeRegistry::set_ron
|
|
pub trait Reflect {
|
|
/// Static descriptors for every reflected field, in declaration order.
|
|
fn fields(&self) -> &'static [FieldInfo];
|
|
|
|
/// Serialize one field's current value to RON, or `None` if no field of
|
|
/// that name is reflected.
|
|
fn get_field(&self, name: &str) -> Option<String>;
|
|
|
|
/// Parse `value` (RON) into the named field, replacing it.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownField`](ReflectError::UnknownField) if the name isn't a
|
|
/// reflected field, or [`FieldParse`](ReflectError::FieldParse) if the
|
|
/// text isn't valid for the field's type.
|
|
fn set_field(&mut self, name: &str, value: &str) -> Result<(), ReflectError>;
|
|
}
|
|
|
|
/// A fieldless enum whose variants can be listed by name.
|
|
///
|
|
/// Per-field reflection tells the inspector a field's *type name* but not, for
|
|
/// an enum-typed field, the set of values it may take. `ReflectEnum` supplies
|
|
/// that list so the inspector can render a dropdown instead of a free-text RON
|
|
/// box. Register the enum with
|
|
/// [`register_enum`](TypeRegistry::register_enum) and the inspector looks its
|
|
/// variants up by type name. Derive it with `#[derive(ReflectEnum)]` (unit
|
|
/// variants only).
|
|
pub trait ReflectEnum {
|
|
/// The enum's variant names, in declaration order. Each is valid RON for
|
|
/// the corresponding unit variant, so it round-trips through
|
|
/// [`get_field`](TypeRegistry::get_field) / [`set_field`](TypeRegistry::set_field).
|
|
fn variants() -> &'static [&'static str];
|
|
}
|
|
|
|
/// A static description of one reflected field.
|
|
///
|
|
/// `type_name` is the field type's *syntactic* spelling (e.g. `"f32"`,
|
|
/// `"bool"`, `"Vec3"`, `"Handle < Font >"`) as written in the source. A
|
|
/// generic inspector dispatches a widget on it and falls back to a raw RON
|
|
/// editor for types it doesn't recognize.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct FieldInfo {
|
|
/// The field's identifier.
|
|
pub name: &'static str,
|
|
/// The field type's syntactic name.
|
|
pub type_name: &'static str,
|
|
/// `(min, max)` bounds set with `#[reflect(min = X, max = Y)]`. The
|
|
/// inspector uses this to render a `Slider` for a `f32` field whose value
|
|
/// is normalized (e.g. metallic / roughness in `0..=1`); for plain numeric
|
|
/// fields it stays `None` and a `DragValue` is used instead.
|
|
pub range: Option<(f32, f32)>,
|
|
}
|
|
|
|
/// Implementation detail of `#[derive(Reflect)]` — serialize a field to RON.
|
|
///
|
|
/// Generated code calls this so it never needs `ron` in scope itself.
|
|
#[doc(hidden)]
|
|
pub fn __reflect_to_ron<T: Serialize>(value: &T) -> Option<String> {
|
|
ron::to_string(value).ok()
|
|
}
|
|
|
|
/// Implementation detail of `#[derive(Reflect)]` — parse a field from RON.
|
|
#[doc(hidden)]
|
|
pub fn __reflect_from_ron<T: DeserializeOwned>(
|
|
field: &str,
|
|
value: &str,
|
|
) -> Result<T, ReflectError> {
|
|
ron::from_str(value).map_err(|err| ReflectError::FieldParse {
|
|
field: field.to_string(),
|
|
message: err.to_string(),
|
|
})
|
|
}
|
|
|
|
/// The monomorphized operations for one registered type, stored as plain
|
|
/// function pointers (the closures capture nothing, so they coerce to `fn`).
|
|
struct ReflectedType {
|
|
get_ron: fn(&World, Entity) -> Option<String>,
|
|
set_ron: fn(&mut World, Entity, &str) -> Result<(), String>,
|
|
has: fn(&World, Entity) -> bool,
|
|
remove: fn(&mut World, Entity) -> bool,
|
|
/// Per-field operations, present only for types registered with
|
|
/// [`register_reflected`](TypeRegistry::register_reflected) (i.e. `T:
|
|
/// Reflect`). `None` for whole-value-only types. The registry callers
|
|
/// guarantee the component is present before invoking `get`/`set`.
|
|
fields: Option<FieldOps>,
|
|
/// Inserts a `T::default()` on an entity, present only for types registered
|
|
/// with [`register_addable`](TypeRegistry::register_addable) (i.e. `T:
|
|
/// Default`). `None` means the type can't be added from a generic "Add
|
|
/// Component" menu (no zero-arg construction).
|
|
add_default: Option<fn(&mut World, Entity)>,
|
|
}
|
|
|
|
/// The `T: Reflect` field operations, type-erased to function pointers.
|
|
struct FieldOps {
|
|
infos: fn(&World, Entity) -> Option<&'static [FieldInfo]>,
|
|
get: fn(&World, Entity, &str) -> Option<String>,
|
|
set: fn(&mut World, Entity, &str, &str) -> Result<(), ReflectError>,
|
|
}
|
|
|
|
/// A registry mapping stable type names to type-erased component operations.
|
|
///
|
|
/// Owned by the app/module system (Stage 5): each module registers the component
|
|
/// types it introduces, so the editor and scripts can address any of them by
|
|
/// name. Names are the identity used in serialized data and UI, so they should
|
|
/// be stable across versions.
|
|
#[derive(Default)]
|
|
pub struct TypeRegistry {
|
|
types: BTreeMap<&'static str, ReflectedType>,
|
|
/// Variant lists for registered enum types, keyed by the same syntactic
|
|
/// type name a [`FieldInfo::type_name`] carries, so the inspector can turn
|
|
/// an enum-typed field into a dropdown.
|
|
enums: BTreeMap<&'static str, &'static [&'static str]>,
|
|
}
|
|
|
|
impl TypeRegistry {
|
|
/// An empty registry.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Registers component type `T` under `name`.
|
|
///
|
|
/// `T` must be an ECS component (`Send + Sync + 'static`) and round-trip
|
|
/// through `serde`. Re-registering the same name replaces the entry.
|
|
pub fn register<T>(&mut self, name: &'static str)
|
|
where
|
|
T: Component + Serialize + DeserializeOwned,
|
|
{
|
|
self.types.insert(
|
|
name,
|
|
ReflectedType {
|
|
get_ron: |world, e| {
|
|
world
|
|
.get::<&T>(e)
|
|
.ok()
|
|
.and_then(|c| ron::to_string(&*c).ok())
|
|
},
|
|
set_ron: |world, e, text| {
|
|
let value: T = ron::from_str(text).map_err(|err| err.to_string())?;
|
|
// `contains` is checked by the caller, so insert cannot fail
|
|
// for a missing entity; map defensively all the same.
|
|
world
|
|
.insert_one(e, value)
|
|
.map_err(|_| "entity is not live".to_string())
|
|
},
|
|
has: |world, e| world.get::<&T>(e).is_ok(),
|
|
remove: |world, e| world.remove_one::<T>(e).is_ok(),
|
|
fields: None,
|
|
add_default: None,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Registers component type `T` with **per-field** reflection in addition
|
|
/// to whole-value access.
|
|
///
|
|
/// Identical to [`register`](Self::register) but also wires the
|
|
/// [`Reflect`] field operations, so [`field_infos`](Self::field_infos) /
|
|
/// [`get_field`](Self::get_field) / [`set_field`](Self::set_field) work for
|
|
/// this type. This is what lets the editor render a widget per field. Use
|
|
/// it for any type whose fields should be individually editable; use
|
|
/// `register` for opaque types edited only as a whole.
|
|
pub fn register_reflected<T>(&mut self, name: &'static str)
|
|
where
|
|
T: Component + Serialize + DeserializeOwned + Reflect,
|
|
{
|
|
self.types.insert(
|
|
name,
|
|
ReflectedType {
|
|
get_ron: |world, e| {
|
|
world
|
|
.get::<&T>(e)
|
|
.ok()
|
|
.and_then(|c| ron::to_string(&*c).ok())
|
|
},
|
|
set_ron: |world, e, text| {
|
|
let value: T = ron::from_str(text).map_err(|err| err.to_string())?;
|
|
world
|
|
.insert_one(e, value)
|
|
.map_err(|_| "entity is not live".to_string())
|
|
},
|
|
has: |world, e| world.get::<&T>(e).is_ok(),
|
|
remove: |world, e| world.remove_one::<T>(e).is_ok(),
|
|
fields: Some(FieldOps {
|
|
infos: |world, e| world.get::<&T>(e).ok().map(|c| c.fields()),
|
|
get: |world, e, field| world.get::<&T>(e).ok().and_then(|c| c.get_field(field)),
|
|
set: |world, e, field, ron| {
|
|
// The registry verifies the component is present before
|
|
// calling, so this access cannot fail.
|
|
let mut c = world
|
|
.get::<&mut T>(e)
|
|
.expect("component present (checked by caller)");
|
|
c.set_field(field, ron)
|
|
},
|
|
}),
|
|
add_default: None,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Registers a reflected component type that can also be **added from a
|
|
/// generic "Add Component" menu** — `T` must be `Default`, which supplies
|
|
/// the value inserted on the entity.
|
|
///
|
|
/// Equivalent to [`register_reflected`](Self::register_reflected) plus a
|
|
/// zero-arg constructor. Use it for components a user can attach in the
|
|
/// editor; use `register_reflected` for components that only exist
|
|
/// implicitly (every entity already has them) or that have no sensible
|
|
/// default.
|
|
pub fn register_addable<T>(&mut self, name: &'static str)
|
|
where
|
|
T: Component + Serialize + DeserializeOwned + Reflect + Default,
|
|
{
|
|
self.register_reflected::<T>(name);
|
|
if let Some(reflected) = self.types.get_mut(name) {
|
|
reflected.add_default = Some(|world, e| {
|
|
let _ = world.insert_one(e, T::default());
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Removes the type registered under `name`. Returns whether it existed.
|
|
pub fn unregister(&mut self, name: &str) -> bool {
|
|
self.types.remove(name).is_some()
|
|
}
|
|
|
|
/// Whether a type is registered under `name`.
|
|
pub fn is_registered(&self, name: &str) -> bool {
|
|
self.types.contains_key(name)
|
|
}
|
|
|
|
/// The number of registered types.
|
|
pub fn len(&self) -> usize {
|
|
self.types.len()
|
|
}
|
|
|
|
/// Whether no types are registered.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.types.is_empty()
|
|
}
|
|
|
|
/// The names of every registered type, sorted.
|
|
pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
|
|
self.types.keys().copied()
|
|
}
|
|
|
|
/// Serializes the named component on `entity` to RON.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered,
|
|
/// [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead, or
|
|
/// [`Missing`](ReflectError::Missing) if the entity lacks the component.
|
|
pub fn get_ron(
|
|
&self,
|
|
world: &World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
) -> Result<String, ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
(reflected.get_ron)(world, entity)
|
|
.ok_or_else(|| ReflectError::Missing(type_name.to_string()))
|
|
}
|
|
|
|
/// Parses `ron` into the named type and writes it onto `entity`, inserting
|
|
/// the component if absent or replacing it if present.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType), [`NoSuchEntity`](ReflectError::NoSuchEntity),
|
|
/// or [`Parse`](ReflectError::Parse) if the text is not valid for the type.
|
|
pub fn set_ron(
|
|
&self,
|
|
world: &mut World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
ron: &str,
|
|
) -> Result<(), ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
(reflected.set_ron)(world, entity, ron).map_err(|message| ReflectError::Parse {
|
|
type_name: type_name.to_string(),
|
|
message,
|
|
})
|
|
}
|
|
|
|
/// Whether `entity` carries the named component.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered.
|
|
pub fn has(
|
|
&self,
|
|
world: &World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
) -> Result<bool, ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
Ok(world.contains(entity) && (reflected.has)(world, entity))
|
|
}
|
|
|
|
/// Removes the named component from `entity`. Returns whether it was present.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType) if the name is not registered.
|
|
pub fn remove(
|
|
&self,
|
|
world: &mut World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
) -> Result<bool, ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Ok(false);
|
|
}
|
|
Ok((reflected.remove)(world, entity))
|
|
}
|
|
|
|
/// The names of all *registered* component types currently on `entity`,
|
|
/// sorted. This is what a generic inspector iterates to show every editable
|
|
/// component without knowing any concrete types.
|
|
pub fn components_on(&self, world: &World, entity: Entity) -> Vec<&'static str> {
|
|
if !world.contains(entity) {
|
|
return Vec::new();
|
|
}
|
|
self.types
|
|
.iter()
|
|
.filter(|(_, r)| (r.has)(world, entity))
|
|
.map(|(name, _)| *name)
|
|
.collect()
|
|
}
|
|
|
|
/// Whether the named type was registered with per-field reflection
|
|
/// ([`register_reflected`](Self::register_reflected)).
|
|
pub fn is_reflected(&self, type_name: &str) -> bool {
|
|
matches!(self.types.get(type_name), Some(r) if r.fields.is_some())
|
|
}
|
|
|
|
/// Registers a fieldless enum `E` under `name` (the syntactic type name its
|
|
/// fields carry), so [`enum_variants`](Self::enum_variants) can list its
|
|
/// values for an inspector dropdown. Independent of component registration —
|
|
/// an enum is a field *type*, not a component.
|
|
pub fn register_enum<E>(&mut self, name: &'static str)
|
|
where
|
|
E: ReflectEnum,
|
|
{
|
|
self.enums.insert(name, E::variants());
|
|
}
|
|
|
|
/// The variant names of an enum type registered with
|
|
/// [`register_enum`](Self::register_enum), or `None` if the type name isn't
|
|
/// a registered enum. The inspector renders a dropdown when this is `Some`.
|
|
pub fn enum_variants(&self, type_name: &str) -> Option<&'static [&'static str]> {
|
|
self.enums.get(type_name).copied()
|
|
}
|
|
|
|
/// Whether the named type can be added from a generic "Add Component" menu
|
|
/// ([`register_addable`](Self::register_addable)).
|
|
pub fn is_addable(&self, type_name: &str) -> bool {
|
|
matches!(self.types.get(type_name), Some(r) if r.add_default.is_some())
|
|
}
|
|
|
|
/// The names of every addable component type, sorted — what an "Add
|
|
/// Component" menu lists.
|
|
pub fn addable_names(&self) -> impl Iterator<Item = &'static str> + '_ {
|
|
self.types
|
|
.iter()
|
|
.filter(|(_, r)| r.add_default.is_some())
|
|
.map(|(name, _)| *name)
|
|
}
|
|
|
|
/// Adds a default-constructed instance of the named component to `entity`,
|
|
/// if the type is addable and the entity doesn't already carry it. Returns
|
|
/// whether a component was inserted.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType) if the name isn't registered,
|
|
/// or [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead.
|
|
pub fn add_default(
|
|
&self,
|
|
world: &mut World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
) -> Result<bool, ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
let Some(make) = reflected.add_default else {
|
|
return Ok(false);
|
|
};
|
|
// Don't clobber an existing component — "add" is a no-op if present.
|
|
if (reflected.has)(world, entity) {
|
|
return Ok(false);
|
|
}
|
|
make(world, entity);
|
|
Ok(true)
|
|
}
|
|
|
|
/// The field descriptors of the named component on `entity`.
|
|
///
|
|
/// This is what a generic inspector iterates to render one widget per
|
|
/// field. Returns [`NotReflected`](ReflectError::NotReflected) for types
|
|
/// registered for whole-value access only.
|
|
///
|
|
/// # Errors
|
|
/// [`UnknownType`](ReflectError::UnknownType),
|
|
/// [`NoSuchEntity`](ReflectError::NoSuchEntity),
|
|
/// [`NotReflected`](ReflectError::NotReflected), or
|
|
/// [`Missing`](ReflectError::Missing) if the entity lacks the component.
|
|
pub fn field_infos(
|
|
&self,
|
|
world: &World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
) -> Result<&'static [FieldInfo], ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
let ops = reflected
|
|
.fields
|
|
.as_ref()
|
|
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
|
|
(ops.infos)(world, entity).ok_or_else(|| ReflectError::Missing(type_name.to_string()))
|
|
}
|
|
|
|
/// Serializes one field of the named component on `entity` to RON.
|
|
///
|
|
/// # Errors
|
|
/// As [`field_infos`](Self::field_infos), plus
|
|
/// [`UnknownField`](ReflectError::UnknownField) if the type has no such
|
|
/// field.
|
|
pub fn get_field(
|
|
&self,
|
|
world: &World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
field: &str,
|
|
) -> Result<String, ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
let ops = reflected
|
|
.fields
|
|
.as_ref()
|
|
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
|
|
if !(reflected.has)(world, entity) {
|
|
return Err(ReflectError::Missing(type_name.to_string()));
|
|
}
|
|
(ops.get)(world, entity, field).ok_or_else(|| ReflectError::UnknownField(field.to_string()))
|
|
}
|
|
|
|
/// Parses `ron` into one field of the named component on `entity`.
|
|
///
|
|
/// Only the named field changes; the rest of the component is untouched —
|
|
/// this is the granularity an inspector edit needs.
|
|
///
|
|
/// # Errors
|
|
/// As [`field_infos`](Self::field_infos), plus
|
|
/// [`UnknownField`](ReflectError::UnknownField) or
|
|
/// [`FieldParse`](ReflectError::FieldParse).
|
|
pub fn set_field(
|
|
&self,
|
|
world: &mut World,
|
|
entity: Entity,
|
|
type_name: &str,
|
|
field: &str,
|
|
ron: &str,
|
|
) -> Result<(), ReflectError> {
|
|
let reflected = self.lookup(type_name)?;
|
|
if !world.contains(entity) {
|
|
return Err(ReflectError::NoSuchEntity);
|
|
}
|
|
let ops = reflected
|
|
.fields
|
|
.as_ref()
|
|
.ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?;
|
|
if !(reflected.has)(world, entity) {
|
|
return Err(ReflectError::Missing(type_name.to_string()));
|
|
}
|
|
(ops.set)(world, entity, field, ron)
|
|
}
|
|
|
|
fn lookup(&self, type_name: &str) -> Result<&ReflectedType, ReflectError> {
|
|
self.types
|
|
.get(type_name)
|
|
.ok_or_else(|| ReflectError::UnknownType(type_name.to_string()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::math::{Transform, Vec3};
|
|
use crate::scene::{Node, Scene};
|
|
use serde::Deserialize;
|
|
|
|
fn registry() -> TypeRegistry {
|
|
let mut r = TypeRegistry::new();
|
|
r.register_reflected::<Transform>("Transform");
|
|
r.register_reflected::<Node>("Node");
|
|
r
|
|
}
|
|
|
|
#[test]
|
|
fn registration_is_listed_and_sorted() {
|
|
let r = registry();
|
|
assert!(r.is_registered("Transform"));
|
|
assert!(!r.is_registered("Nope"));
|
|
assert_eq!(r.len(), 2);
|
|
assert_eq!(r.names().collect::<Vec<_>>(), vec!["Node", "Transform"]);
|
|
}
|
|
|
|
#[test]
|
|
fn get_then_set_round_trips_generically() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn(
|
|
"thing",
|
|
Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)),
|
|
);
|
|
|
|
// Read generically (no Transform type named at this call site beyond the
|
|
// string), then write it straight back.
|
|
let ron = r.get_ron(scene.world(), e, "Transform").unwrap();
|
|
r.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap();
|
|
|
|
// The value survived the round trip.
|
|
let after = scene.local_transform(e).unwrap();
|
|
assert!((after.translation - Vec3::new(1.0, 2.0, 3.0)).length() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn set_can_mutate_through_the_text() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
|
|
// Hand-edit the serialized form (as the inspector / a script would) and
|
|
// apply it.
|
|
let edited =
|
|
ron::to_string(&Transform::from_translation(Vec3::new(5.0, 0.0, 0.0))).unwrap();
|
|
r.set_ron(scene.world_mut(), e, "Transform", &edited)
|
|
.unwrap();
|
|
assert!((scene.local_transform(e).unwrap().translation.x - 5.0).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn components_on_lists_present_registered_types() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY); // has Node + Transform
|
|
assert_eq!(r.components_on(scene.world(), e), vec!["Node", "Transform"]);
|
|
|
|
// Removing one drops it from the listing.
|
|
assert!(r.remove(scene.world_mut(), e, "Transform").unwrap());
|
|
assert_eq!(r.components_on(scene.world(), e), vec!["Node"]);
|
|
assert!(!r.has(scene.world(), e, "Transform").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn errors_are_specific() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
|
|
// Unknown type name.
|
|
assert!(matches!(
|
|
r.get_ron(scene.world(), e, "Ghost"),
|
|
Err(ReflectError::UnknownType(_))
|
|
));
|
|
// Live entity missing the component.
|
|
scene.world_mut().remove_one::<Node>(e).unwrap();
|
|
assert!(matches!(
|
|
r.get_ron(scene.world(), e, "Node"),
|
|
Err(ReflectError::Missing(_))
|
|
));
|
|
// Dead entity.
|
|
let dead = scene.spawn("dead", Transform::IDENTITY);
|
|
scene.despawn(dead, crate::scene::DespawnPolicy::Recursive);
|
|
assert!(matches!(
|
|
r.get_ron(scene.world(), dead, "Transform"),
|
|
Err(ReflectError::NoSuchEntity)
|
|
));
|
|
// Malformed RON.
|
|
assert!(matches!(
|
|
r.set_ron(scene.world_mut(), e, "Transform", "not valid ron"),
|
|
Err(ReflectError::Parse { .. })
|
|
));
|
|
}
|
|
|
|
// --- Per-field reflection (`#[derive(Reflect)]`) ---
|
|
|
|
/// A representative component: a mix of field types, a skipped public
|
|
/// field, and a private field — exercises the derive's selection rules.
|
|
#[derive(Reflect, Serialize, Deserialize, PartialEq, Debug)]
|
|
struct Timer {
|
|
pub repeating: bool,
|
|
pub duration: f32,
|
|
pub label: String,
|
|
#[reflect(skip)]
|
|
pub elapsed: f32,
|
|
// Private: never reflected regardless of `skip`.
|
|
_internal: u32,
|
|
}
|
|
|
|
impl Timer {
|
|
fn sample() -> Self {
|
|
Self {
|
|
repeating: true,
|
|
duration: 2.5,
|
|
label: "tick".to_string(),
|
|
elapsed: 1.0,
|
|
_internal: 7,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A struct whose normalized fields carry slider ranges via the new
|
|
/// `#[reflect(min, max)]` attribute. The inspector dispatches a `Slider`
|
|
/// instead of a `DragValue` when both bounds are present.
|
|
#[derive(Reflect, Serialize, Deserialize)]
|
|
struct Knobs {
|
|
#[reflect(min = 0.0, max = 1.0)]
|
|
pub gain: f32,
|
|
pub bias: f32,
|
|
}
|
|
|
|
#[derive(Reflect, Serialize, Deserialize)]
|
|
struct Wrap(pub i32, pub bool);
|
|
|
|
#[test]
|
|
fn derive_supports_tuple_structs_with_positional_field_names() {
|
|
// Tuple-struct field names round-trip as "0", "1", ... — matching
|
|
// Rust's own positional accessors. Lets one-field newtype components
|
|
// like `Layer(pub LayerMask)` reflect without a wrapper.
|
|
let mut w = Wrap(42, false);
|
|
let names: Vec<_> = w.fields().iter().map(|f| f.name).collect();
|
|
assert_eq!(names, ["0", "1"]);
|
|
assert_eq!(w.get_field("0").as_deref(), Some("42"));
|
|
w.set_field("1", "true").unwrap();
|
|
assert!(w.1);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_captures_min_max_attributes_as_field_range() {
|
|
let k = Knobs {
|
|
gain: 0.5,
|
|
bias: 0.0,
|
|
};
|
|
let fields = k.fields();
|
|
let gain = fields.iter().find(|f| f.name == "gain").unwrap();
|
|
let bias = fields.iter().find(|f| f.name == "bias").unwrap();
|
|
assert_eq!(gain.range, Some((0.0_f32, 1.0_f32)));
|
|
assert_eq!(bias.range, None);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_lists_only_public_non_skipped_fields_in_order() {
|
|
let t = Timer::sample();
|
|
let names: Vec<_> = t.fields().iter().map(|f| f.name).collect();
|
|
assert_eq!(names, ["repeating", "duration", "label"]);
|
|
// Syntactic type names are preserved for inspector widget dispatch.
|
|
let types: Vec<_> = t.fields().iter().map(|f| f.type_name).collect();
|
|
assert_eq!(types, ["bool", "f32", "String"]);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_gets_each_field_as_ron() {
|
|
let t = Timer::sample();
|
|
assert_eq!(t.get_field("repeating").as_deref(), Some("true"));
|
|
assert_eq!(t.get_field("duration").as_deref(), Some("2.5"));
|
|
assert_eq!(t.get_field("label").as_deref(), Some("\"tick\""));
|
|
// Skipped + private + unknown all read as None.
|
|
assert_eq!(t.get_field("elapsed"), None);
|
|
assert_eq!(t.get_field("_internal"), None);
|
|
assert_eq!(t.get_field("nope"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_sets_a_single_field_without_touching_others() {
|
|
let mut t = Timer::sample();
|
|
t.set_field("duration", "9.0").unwrap();
|
|
t.set_field("repeating", "false").unwrap();
|
|
assert_eq!(t.duration, 9.0);
|
|
assert!(!t.repeating);
|
|
// Other fields are untouched.
|
|
assert_eq!(t.label, "tick");
|
|
assert_eq!(t.elapsed, 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_set_reports_unknown_field_and_parse_errors() {
|
|
let mut t = Timer::sample();
|
|
assert!(matches!(
|
|
t.set_field("elapsed", "0.0"), // public but skipped → not reflected
|
|
Err(ReflectError::UnknownField(f)) if f == "elapsed"
|
|
));
|
|
assert!(matches!(
|
|
t.set_field("missing", "0.0"),
|
|
Err(ReflectError::UnknownField(_))
|
|
));
|
|
assert!(matches!(
|
|
t.set_field("duration", "not a float"),
|
|
Err(ReflectError::FieldParse { field, .. }) if field == "duration"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn registry_lists_fields_of_a_reflected_component() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
assert!(r.is_reflected("Transform"));
|
|
let names: Vec<_> = r
|
|
.field_infos(scene.world(), e, "Transform")
|
|
.unwrap()
|
|
.iter()
|
|
.map(|f| f.name)
|
|
.collect();
|
|
assert_eq!(names, ["translation", "rotation", "scale"]);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_gets_and_sets_one_field_through_the_world() {
|
|
let r = registry();
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
|
|
// Set just the translation; rotation/scale stay identity. glam's Vec3
|
|
// serializes as a tuple, so RON is `(1.0,2.0,3.0)`.
|
|
r.set_field(
|
|
scene.world_mut(),
|
|
e,
|
|
"Transform",
|
|
"translation",
|
|
"(1.0, 2.0, 3.0)",
|
|
)
|
|
.unwrap();
|
|
let t = scene.world().get::<&Transform>(e).unwrap();
|
|
assert_eq!(t.translation, Vec3::new(1.0, 2.0, 3.0));
|
|
assert_eq!(t.scale, Vec3::ONE);
|
|
drop(t);
|
|
|
|
let got = r
|
|
.get_field(scene.world(), e, "Transform", "translation")
|
|
.unwrap();
|
|
assert_eq!(got, "(1.0,2.0,3.0)");
|
|
}
|
|
|
|
#[test]
|
|
fn registry_field_access_errors_are_specific() {
|
|
let mut r = registry();
|
|
// A whole-value-only type → NotReflected on field access.
|
|
r.register::<TimerWhole>("TimerWhole");
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
scene.world_mut().insert_one(e, TimerWhole(1)).unwrap();
|
|
|
|
assert!(!r.is_reflected("TimerWhole"));
|
|
assert!(matches!(
|
|
r.field_infos(scene.world(), e, "TimerWhole"),
|
|
Err(ReflectError::NotReflected(_))
|
|
));
|
|
// Unknown field on a reflected type.
|
|
assert!(matches!(
|
|
r.get_field(scene.world(), e, "Transform", "nope"),
|
|
Err(ReflectError::UnknownField(_))
|
|
));
|
|
// Reflected type, but the entity lacks the component.
|
|
let bare = scene.spawn("bare", Transform::IDENTITY);
|
|
scene.world_mut().remove_one::<Node>(bare).unwrap();
|
|
assert!(matches!(
|
|
r.get_field(scene.world(), bare, "Node", "name"),
|
|
Err(ReflectError::Missing(_))
|
|
));
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct TimerWhole(u32);
|
|
|
|
// --- Enum reflection (`#[derive(ReflectEnum)]`) ---
|
|
|
|
#[derive(ReflectEnum, Serialize, Deserialize, PartialEq, Debug)]
|
|
enum Facing {
|
|
North,
|
|
East,
|
|
South,
|
|
West,
|
|
}
|
|
|
|
#[test]
|
|
fn derive_enum_lists_variants_in_order() {
|
|
assert_eq!(Facing::variants(), &["North", "East", "South", "West"]);
|
|
}
|
|
|
|
#[test]
|
|
fn variant_names_round_trip_as_ron() {
|
|
// The names ReflectEnum returns must be valid RON for the variant, so
|
|
// the inspector can write a chosen name straight back through set_field.
|
|
for name in Facing::variants() {
|
|
let value: Facing = ron::from_str(name).unwrap();
|
|
assert_eq!(&ron::to_string(&value).unwrap(), name);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn addable_component_can_be_added_by_name_and_listed() {
|
|
use crate::render::MeshRenderer;
|
|
let mut r = registry();
|
|
r.register_addable::<MeshRenderer>("MeshRenderer");
|
|
|
|
// Listed as addable; Transform (register_reflected) is not.
|
|
assert!(r.is_addable("MeshRenderer"));
|
|
assert!(!r.is_addable("Transform"));
|
|
let addable: Vec<_> = r.addable_names().collect();
|
|
assert_eq!(addable, ["MeshRenderer"]);
|
|
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
assert!(!r.has(scene.world(), e, "MeshRenderer").unwrap());
|
|
|
|
// First add inserts the default; a second add is a no-op (already there).
|
|
assert!(r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap());
|
|
assert!(r.has(scene.world(), e, "MeshRenderer").unwrap());
|
|
assert!(!r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap());
|
|
|
|
// Its enum field is editable as a registered enum.
|
|
r.register_enum::<crate::render::PrimitiveShape>("PrimitiveShape");
|
|
let shape = r
|
|
.get_field(scene.world(), e, "MeshRenderer", "shape")
|
|
.unwrap();
|
|
assert_eq!(shape, "Cube"); // PrimitiveShape::default()
|
|
assert_eq!(
|
|
r.enum_variants("PrimitiveShape"),
|
|
Some(["Cube", "Sphere", "Plane"].as_slice())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_addable_type_add_default_is_a_noop() {
|
|
let r = registry(); // Transform/Node are register_reflected, not addable
|
|
let mut scene = Scene::new();
|
|
let e = scene.spawn("thing", Transform::IDENTITY);
|
|
// Transform isn't addable → Ok(false), nothing inserted.
|
|
assert!(!r.add_default(scene.world_mut(), e, "Transform").unwrap());
|
|
// Unknown type → error.
|
|
assert!(matches!(
|
|
r.add_default(scene.world_mut(), e, "Ghost"),
|
|
Err(ReflectError::UnknownType(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn registry_lists_enum_variants_by_type_name() {
|
|
let mut r = registry();
|
|
r.register_enum::<Facing>("Facing");
|
|
assert_eq!(
|
|
r.enum_variants("Facing"),
|
|
Some(["North", "East", "South", "West"].as_slice())
|
|
);
|
|
// Unregistered / non-enum type names return None.
|
|
assert_eq!(r.enum_variants("Transform"), None);
|
|
assert_eq!(r.enum_variants("Nope"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_field_values_round_trip_through_get_then_set() {
|
|
let original = Timer::sample();
|
|
let mut clone = Timer {
|
|
repeating: false,
|
|
duration: 0.0,
|
|
label: String::new(),
|
|
elapsed: 0.0,
|
|
_internal: 0,
|
|
};
|
|
for field in original.fields() {
|
|
let ron = original.get_field(field.name).unwrap();
|
|
clone.set_field(field.name, &ron).unwrap();
|
|
}
|
|
// Every reflected field now matches; non-reflected fields keep clone's.
|
|
assert_eq!(clone.repeating, original.repeating);
|
|
assert_eq!(clone.duration, original.duration);
|
|
assert_eq!(clone.label, original.label);
|
|
assert_eq!(clone.elapsed, 0.0);
|
|
}
|
|
}
|