Skip to main content

kvas_engine/
reflect.rs

1//! Tiny reflection layer. A component (or script) lists its editable fields as `Field`s;
2//! the editor draws them and the scene/script layer converts them to and from JSON.
3//! One mechanism, no per-type UI or serialization code.
4
5use glam::Vec3;
6use serde_json::{json, Value};
7
8/// Asset picker filter used by the Inspector.
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum AssetKind {
11    /// Geometry asset (OBJ, FBX, glTF or GLB).
12    Mesh,
13    /// Image asset or procedural texture.
14    Texture,
15    /// WGSL shader asset.
16    Shader,
17    /// Sound clip (wav, ogg, mp3 or flac).
18    Audio,
19}
20
21/// Conversion between enum variants and Inspector dropdown indices.
22pub trait Choice: Copy {
23    /// Ordered labels matching the enum's declaration order.
24    const OPTIONS: &'static [&'static str];
25    /// Index of this variant in [`Self::OPTIONS`].
26    fn to_index(self) -> usize;
27    /// Variant at an index; [`crate::choice_enum!`] clamps out-of-range indices to the last variant.
28    fn from_index(i: usize) -> Self;
29}
30
31/// Declares a serde enum that can be picked from a dropdown.
32#[macro_export]
33macro_rules! choice_enum {
34    ($(#[$m:meta])* $name:ident { $($v:ident),+ $(,)? } default $d:ident) => {
35        $(#[$m])*
36        #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
37        pub enum $name { $(#[doc = stringify!($v)] $v),+ }
38        impl Default for $name { fn default() -> Self { $name::$d } }
39        impl $crate::reflect::Choice for $name {
40            const OPTIONS: &'static [&'static str] = &[$(stringify!($v)),+];
41            fn to_index(self) -> usize { self as usize }
42            fn from_index(i: usize) -> Self {
43                const ALL: &[$name] = &[$($name::$v),+];
44                ALL[i.min(ALL.len() - 1)]
45            }
46        }
47    };
48}
49
50/// Mutable borrowed value described by a [`Field`].
51/// Descriptors must be dropped before borrowing the original value again.
52pub enum FieldRef<'a> {
53    /// Finite single-precision number.
54    F32(&'a mut f32),
55    /// Signed 32-bit integer.
56    I32(&'a mut i32),
57    /// Unsigned 32-bit integer, including bit masks and deterministic seeds.
58    U32(&'a mut u32),
59    /// Boolean checkbox.
60    Bool(&'a mut bool),
61    /// Three-component vector encoded as a JSON array.
62    Vec3(&'a mut Vec3),
63    /// RGBA colour encoded as four numbers.
64    Color(&'a mut [f32; 4]),
65    /// Editable UTF-8 string.
66    Text(&'a mut String),
67    /// Asset-relative string with a picker category.
68    Asset(&'a mut String, AssetKind),
69    /// Name of another entity in the scene.
70    Entity(&'a mut String),
71    /// Dropdown state and callback to write a selected variant.
72    Choice {
73        /// Ordered display labels.
74        options: &'static [&'static str],
75        /// Current selection; must index `options`.
76        index: usize,
77        /// Writes a selection into the borrowed original enum.
78        set: Box<dyn FnMut(usize) + 'a>,
79    },
80}
81
82/// Named editable property shared by Inspector widgets and scene script properties.
83///
84/// ```
85/// use kvas_engine::reflect::Field;
86/// let mut speed = 2.0;
87/// { let mut field = Field::f32("speed", &mut speed);
88///   field.set_json(&serde_json::json!(3.0)); }
89/// assert_eq!(speed, 3.0);
90/// ```
91pub struct Field<'a> {
92    /// Stable property key used in scene JSON, undo and the Inspector.
93    pub name: &'static str,
94    /// Borrowed value and editor presentation kind.
95    pub value: FieldRef<'a>,
96}
97
98impl<'a> Field<'a> {
99    /// Borrows a floating-point property under a stable field name.
100    pub fn f32(name: &'static str, v: &'a mut f32) -> Self {
101        Self { name, value: FieldRef::F32(v) }
102    }
103    /// Borrows a signed integer property.
104    pub fn i32(name: &'static str, v: &'a mut i32) -> Self {
105        Self { name, value: FieldRef::I32(v) }
106    }
107    /// Borrows an unsigned integer property without narrowing its range to i32.
108    pub fn u32(name: &'static str, value: &'a mut u32) -> Self {
109        Self { name, value: FieldRef::U32(value) }
110    }
111    /// Borrows a boolean checkbox property.
112    pub fn bool(name: &'static str, v: &'a mut bool) -> Self {
113        Self { name, value: FieldRef::Bool(v) }
114    }
115    /// Borrows a three-component vector property.
116    pub fn vec3(name: &'static str, v: &'a mut Vec3) -> Self {
117        Self { name, value: FieldRef::Vec3(v) }
118    }
119    /// Borrows an RGBA colour property.
120    pub fn color(name: &'static str, v: &'a mut [f32; 4]) -> Self {
121        Self { name, value: FieldRef::Color(v) }
122    }
123    /// Borrows an editable string property.
124    pub fn text(name: &'static str, v: &'a mut String) -> Self {
125        Self { name, value: FieldRef::Text(v) }
126    }
127    /// Borrows an asset path with the specified picker category.
128    pub fn asset(name: &'static str, v: &'a mut String, kind: AssetKind) -> Self {
129        Self { name, value: FieldRef::Asset(v, kind) }
130    }
131    /// Borrows an entity-name reference; names are resolved by gameplay code.
132    pub fn entity(name: &'static str, v: &'a mut String) -> Self {
133        Self { name, value: FieldRef::Entity(v) }
134    }
135    /// Borrows an enum as a dropdown using its Choice implementation.
136    pub fn choice<T: Choice + 'a>(name: &'static str, v: &'a mut T) -> Self {
137        let index = v.to_index();
138        Self { name, value: FieldRef::Choice { options: T::OPTIONS, index, set: Box::new(move |i| *v = T::from_index(i)) } }
139    }
140
141    /// Copies the current reflected value into JSON.
142    /// Non-finite numeric values serialize as null through serde_json.
143    pub fn to_json(&self) -> Value {
144        match &self.value {
145            FieldRef::F32(v) => json!(**v),
146            FieldRef::I32(v) => json!(**v),
147            FieldRef::U32(v) => json!(**v),
148            FieldRef::Bool(v) => json!(**v),
149            FieldRef::Vec3(v) => json!(v.to_array()),
150            FieldRef::Color(v) => json!(**v),
151            FieldRef::Text(v) | FieldRef::Asset(v, _) | FieldRef::Entity(v) => json!(v),
152            FieldRef::Choice { options, index, .. } => json!(options.get(*index)),
153        }
154    }
155
156    /// Applies a JSON value; values of the wrong shape are ignored.
157    pub fn set_json(&mut self, value: &Value) {
158        match &mut self.value {
159            FieldRef::F32(v) => {
160                if let Some(x) = value.as_f64() {
161                    if x.is_finite() && (x as f32).is_finite() {
162                        **v = x as f32
163                    }
164                }
165            }
166            FieldRef::I32(v) => {
167                if let Some(x) = value.as_i64() {
168                    if let Ok(x) = i32::try_from(x) {
169                        **v = x
170                    }
171                }
172            }
173            FieldRef::U32(v) => {
174                if let Some(x) = value.as_u64().and_then(|x| u32::try_from(x).ok()) {
175                    **v = x;
176                }
177            }
178            FieldRef::Bool(v) => {
179                if let Some(x) = value.as_bool() {
180                    **v = x
181                }
182            }
183            FieldRef::Vec3(v) => {
184                if let Some(a) = floats::<3>(value) {
185                    **v = Vec3::from_array(a)
186                }
187            }
188            FieldRef::Color(v) => {
189                if let Some(a) = floats::<4>(value) {
190                    **v = a
191                }
192            }
193            FieldRef::Text(v) | FieldRef::Asset(v, _) | FieldRef::Entity(v) => {
194                if let Some(s) = value.as_str() {
195                    **v = s.to_string()
196                }
197            }
198            FieldRef::Choice { options, index, set } => {
199                if let Some(i) = value.as_str().and_then(|s| options.iter().position(|o| *o == s)) {
200                    *index = i;
201                    set(i);
202                }
203            }
204        }
205    }
206
207    /// Applies a value only if its shape and numeric range match the field.
208    /// Returns `false` without mutation for invalid data, unknown choice labels,
209    /// integer overflow or non-finite float conversions.
210    pub fn try_set_json(&mut self, value: &Value) -> bool {
211        let valid = match &self.value {
212            FieldRef::F32(_) => value.as_f64().is_some_and(|v| v.is_finite() && (v as f32).is_finite()),
213            FieldRef::I32(_) => value.as_i64().is_some_and(|v| i32::try_from(v).is_ok()),
214            FieldRef::U32(_) => value.as_u64().is_some_and(|v| u32::try_from(v).is_ok()),
215            FieldRef::Bool(_) => value.is_boolean(),
216            FieldRef::Vec3(_) => floats::<3>(value).is_some(),
217            FieldRef::Color(_) => floats::<4>(value).is_some(),
218            FieldRef::Text(_) | FieldRef::Asset(_, _) | FieldRef::Entity(_) => value.is_string(),
219            FieldRef::Choice { options, .. } => value.as_str().is_some_and(|s| options.contains(&s)),
220        };
221        if valid {
222            self.set_json(value);
223        }
224        valid
225    }
226}
227
228/// Applies saved properties and returns warnings for unknown or invalid values.
229/// Valid fields still apply when another property is invalid; missing properties
230/// preserve current values. Warning order follows JSON map iteration order.
231pub fn apply_props_checked(fields: &mut [Field], props: &serde_json::Map<String, Value>) -> Vec<String> {
232    let mut warnings = Vec::new();
233    for (name, value) in props {
234        match fields.iter_mut().find(|f| f.name == name) {
235            Some(field) => {
236                if !field.try_set_json(value) {
237                    warnings.push(format!("invalid value for property '{name}'"));
238                }
239            }
240            None => warnings.push(format!("unknown property '{name}'")),
241        }
242    }
243    warnings
244}
245
246fn floats<const N: usize>(v: &Value) -> Option<[f32; N]> {
247    let arr = v.as_array()?;
248    let mut out = [0.0; N];
249    if arr.len() != N {
250        return None;
251    }
252    for (o, x) in out.iter_mut().zip(arr) {
253        *o = x.as_f64()? as f32;
254        if !o.is_finite() {
255            return None;
256        }
257    }
258    Some(out)
259}
260
261/// Writes saved JSON values into matching fields.
262pub fn apply_props(fields: &mut [Field], props: &serde_json::Map<String, Value>) {
263    for f in fields {
264        if let Some(v) = props.get(f.name) {
265            f.set_json(v);
266        }
267    }
268}
269
270/// Snapshots editable values by property name; duplicate names use the last value.
271pub fn read_props(fields: &[Field]) -> serde_json::Map<String, Value> {
272    fields.iter().map(|f| (f.name.to_string(), f.to_json())).collect()
273}