1use glam::Vec3;
6use serde_json::{json, Value};
7
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum AssetKind {
11 Mesh,
13 Texture,
15 Shader,
17 Audio,
19}
20
21pub trait Choice: Copy {
23 const OPTIONS: &'static [&'static str];
25 fn to_index(self) -> usize;
27 fn from_index(i: usize) -> Self;
29}
30
31#[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
50pub enum FieldRef<'a> {
53 F32(&'a mut f32),
55 I32(&'a mut i32),
57 U32(&'a mut u32),
59 Bool(&'a mut bool),
61 Vec3(&'a mut Vec3),
63 Color(&'a mut [f32; 4]),
65 Text(&'a mut String),
67 Asset(&'a mut String, AssetKind),
69 Entity(&'a mut String),
71 Choice {
73 options: &'static [&'static str],
75 index: usize,
77 set: Box<dyn FnMut(usize) + 'a>,
79 },
80}
81
82pub struct Field<'a> {
92 pub name: &'static str,
94 pub value: FieldRef<'a>,
96}
97
98impl<'a> Field<'a> {
99 pub fn f32(name: &'static str, v: &'a mut f32) -> Self {
101 Self { name, value: FieldRef::F32(v) }
102 }
103 pub fn i32(name: &'static str, v: &'a mut i32) -> Self {
105 Self { name, value: FieldRef::I32(v) }
106 }
107 pub fn u32(name: &'static str, value: &'a mut u32) -> Self {
109 Self { name, value: FieldRef::U32(value) }
110 }
111 pub fn bool(name: &'static str, v: &'a mut bool) -> Self {
113 Self { name, value: FieldRef::Bool(v) }
114 }
115 pub fn vec3(name: &'static str, v: &'a mut Vec3) -> Self {
117 Self { name, value: FieldRef::Vec3(v) }
118 }
119 pub fn color(name: &'static str, v: &'a mut [f32; 4]) -> Self {
121 Self { name, value: FieldRef::Color(v) }
122 }
123 pub fn text(name: &'static str, v: &'a mut String) -> Self {
125 Self { name, value: FieldRef::Text(v) }
126 }
127 pub fn asset(name: &'static str, v: &'a mut String, kind: AssetKind) -> Self {
129 Self { name, value: FieldRef::Asset(v, kind) }
130 }
131 pub fn entity(name: &'static str, v: &'a mut String) -> Self {
133 Self { name, value: FieldRef::Entity(v) }
134 }
135 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 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 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 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
228pub 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
261pub 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
270pub fn read_props(fields: &[Field]) -> serde_json::Map<String, Value> {
272 fields.iter().map(|f| (f.name.to_string(), f.to_json())).collect()
273}