1use std::collections::HashSet;
5
6use crate::props::PropValue;
7
8#[derive(Clone, Copy, PartialEq, Debug)]
10pub enum DefaultPropVal {
11 Num(f64),
12 Int(i64),
13 Bool(bool),
14 Vec2(f64, f64),
15 Str(&'static str),
16 Color([u8; 4]),
17}
18
19impl DefaultPropVal {
20 pub fn to_prop_value(&self) -> PropValue {
22 match self {
23 Self::Num(f) => PropValue::Num(*f),
24 Self::Int(i) => PropValue::Int(*i),
25 Self::Bool(b) => PropValue::Bool(*b),
26 Self::Vec2(x, y) => PropValue::Vec2(*x, *y),
27 Self::Str(s) => PropValue::Str((*s).to_string()),
28 Self::Color([r, g, b, a]) => PropValue::Color(*r, *g, *b, *a),
29 }
30 }
31
32 pub fn matches(&self, val: &PropValue) -> bool {
34 match (self, val) {
35 (Self::Num(a), PropValue::Num(b)) => (a - b).abs() < f64::EPSILON,
36 (Self::Int(a), PropValue::Int(b)) => a == b,
37 (Self::Bool(a), PropValue::Bool(b)) => a == b,
38 (Self::Vec2(x1, y1), PropValue::Vec2(x2, y2)) => {
39 (x1 - x2).abs() < f64::EPSILON && (y1 - y2).abs() < f64::EPSILON
40 }
41 (Self::Str(a), PropValue::Str(b)) => *a == b.as_str(),
42 (Self::Color([r1, g1, b1, a1]), PropValue::Color(r2, g2, b2, a2)) => {
43 r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2
44 }
45 _ => false,
46 }
47 }
48}
49
50#[derive(Clone, Copy, PartialEq, Debug)]
52pub enum PropHint {
53 Vec2 { speed: f64 },
54 AngleDeg,
55 Num { speed: f64, min: f64, max: f64 },
56 Int { speed: f64, min: i64, max: i64 },
57 Bool,
58 Text,
59 Enum(&'static [&'static str]),
60 Color,
61 Asset { filter: &'static [&'static str] },
62}
63
64#[derive(Clone, Copy, Debug)]
66pub struct PropSchema {
67 pub key: &'static str,
68 pub label: &'static str,
69 pub doc: &'static str,
74 pub default_value: DefaultPropVal,
75 pub hint: PropHint,
76 pub read_only: bool, }
78
79#[derive(Clone, Copy, Debug)]
81pub struct NodeTypeSchema {
82 pub type_name: &'static str,
83 pub description: &'static str,
86 pub inherits: Option<&'static str>,
87 pub props: &'static [PropSchema],
88 pub abstract_class: bool,
92}
93
94pub use crate::nodes::ALL_NODES as NODE_SCHEMAS;
97
98pub fn node_type_schema(type_name: &str) -> Option<&'static NodeTypeSchema> {
100 NODE_SCHEMAS
101 .iter()
102 .copied()
103 .find(|s| s.type_name == type_name)
104}
105
106pub fn inheritance_chain<'a>(type_name: &'a str) -> Vec<&'a str> {
108 let mut chain = Vec::new();
109 let mut current = Some(type_name);
110 while let Some(curr) = current {
111 chain.push(curr);
112 current = match node_type_schema(curr) {
113 Some(schema) => schema.inherits,
114 None => {
115 if curr != "Node" {
116 Some("Node")
117 } else {
118 None
119 }
120 }
121 };
122 }
123 chain
124}
125
126pub fn type_props(type_name: &str) -> &'static [PropSchema] {
128 node_type_schema(type_name).map(|s| s.props).unwrap_or(&[])
129}
130
131pub fn all_props(type_name: &str) -> Vec<&'static PropSchema> {
137 let mut seen: HashSet<&'static str> = HashSet::new();
138 let mut out = Vec::new();
139 let mut current: Option<String> = Some(type_name.to_string());
140 while let Some(name) = current {
141 if let Some(schema) = node_type_schema(&name) {
142 for prop in schema.props {
143 if seen.insert(prop.key) {
144 out.push(prop);
145 }
146 }
147 current = schema.inherits.map(String::from);
148 } else if name != "Node" {
149 current = Some("Node".to_string());
150 } else {
151 current = None;
152 }
153 }
154 out
155}
156
157pub fn is_valid_custom_prop_type(type_str: &str) -> bool {
160 matches!(
161 type_str,
162 "string"
163 | "str"
164 | "int"
165 | "integer"
166 | "number"
167 | "num"
168 | "float"
169 | "bool"
170 | "boolean"
171 | "vec2"
172 | "color"
173 )
174}
175
176pub fn validate_custom_prop_type(type_str: &str, value: &PropValue) -> Result<(), String> {
180 let want = match type_str {
181 "string" | "str" => matches!(value, PropValue::Str(_)),
182 "int" | "integer" => matches!(value, PropValue::Int(_)),
183 "number" | "num" | "float" => matches!(value, PropValue::Num(_)),
184 "bool" | "boolean" => matches!(value, PropValue::Bool(_)),
185 "vec2" => matches!(value, PropValue::Vec2(_, _)),
186 "color" => matches!(value, PropValue::Color(_, _, _, _)),
187 other => {
188 return Err(format!(
189 "unknown custom prop type {:?} (want one of: {})",
190 other,
191 crate::CUSTOM_PROP_TYPES.join(", ")
192 ));
193 }
194 };
195 if want {
196 Ok(())
197 } else {
198 Err(format!(
199 "expected {}, got {}",
200 canonical_type_name(type_str),
201 value.type_name()
202 ))
203 }
204}
205
206fn canonical_type_name(type_str: &str) -> &'static str {
207 match type_str {
208 "string" | "str" => "string",
209 "int" | "integer" => "int",
210 "number" | "num" | "float" => "number",
211 "bool" | "boolean" => "bool",
212 "vec2" => "vec2",
213 "color" => "color",
214 _ => "unknown",
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_inheritance_chains() {
224 assert_eq!(
225 inheritance_chain("Sprite2D"),
226 vec!["Sprite2D", "Node2D", "Node"]
227 );
228 assert_eq!(
229 inheritance_chain("AnimatedSprite2D"),
230 vec!["AnimatedSprite2D", "Node2D", "Node"]
231 );
232 assert_eq!(
233 inheritance_chain("CharacterBody2D"),
234 vec![
235 "CharacterBody2D",
236 "PhysicsBody2D",
237 "CollisionObject2D",
238 "Node2D",
239 "Node"
240 ]
241 );
242 assert_eq!(
243 inheritance_chain("Button"),
244 vec!["Button", "Control", "Node2D", "Node"]
245 );
246 assert_eq!(inheritance_chain("Node"), vec!["Node"]);
247 assert_eq!(inheritance_chain("Custom"), vec!["Custom", "Node"]);
248 }
249
250 #[test]
251 fn test_type_props() {
252 let props = type_props("Sprite2D");
253 let keys: Vec<&str> = props.iter().map(|p| p.key).collect();
254 assert_eq!(keys, vec!["texture", "atlas", "frame"]);
255
256 let anim_props = type_props("AnimatedSprite2D");
257 let keys: Vec<&str> = anim_props.iter().map(|p| p.key).collect();
258 assert_eq!(
259 keys,
260 vec![
261 "atlas",
262 "animation",
263 "frame",
264 "speed_scale",
265 "playing",
266 "autoplay"
267 ]
268 );
269
270 let rigid = type_props("RigidBody2D");
273 let keys: Vec<&str> = rigid.iter().map(|p| p.key).collect();
274 assert_eq!(keys, vec!["mass", "gravity_scale", "lock_rotation"]);
275 }
276
277 #[test]
278 fn test_all_props_walks_chain() {
279 let keys: Vec<&str> = all_props("Sprite2D").iter().map(|p| p.key).collect();
281 assert_eq!(
282 keys,
283 vec!["texture", "atlas", "frame", "position", "rotation"]
284 );
285
286 let keys: Vec<&str> = all_props("AnimatedSprite2D")
288 .iter()
289 .map(|p| p.key)
290 .collect();
291 assert_eq!(
292 keys,
293 vec![
294 "atlas",
295 "animation",
296 "frame",
297 "speed_scale",
298 "playing",
299 "autoplay",
300 "position",
301 "rotation"
302 ]
303 );
304
305 let keys: Vec<&str> = all_props("Button").iter().map(|p| p.key).collect();
309 assert_eq!(
310 keys,
311 vec![
312 "text",
313 "color",
314 "size",
315 "disabled",
316 "font_size",
317 "position",
318 "rotation"
319 ]
320 );
321 let color = all_props("Button")
323 .into_iter()
324 .find(|p| p.key == "color")
325 .unwrap();
326 assert_eq!(
327 color.default_value,
328 super::DefaultPropVal::Color([51, 65, 85, 255])
329 );
330
331 let keys: Vec<&str> = all_props("CharacterBody2D").iter().map(|p| p.key).collect();
333 assert_eq!(
334 keys,
335 vec![
336 "velocity",
337 "friction",
338 "restitution",
339 "collision_layer",
340 "collision_mask",
341 "position",
342 "rotation"
343 ]
344 );
345
346 assert!(all_props("Custom").is_empty());
348 }
349
350 #[test]
351 fn custom_prop_types_validate() {
352 use crate::PropValue;
353 assert!(is_valid_custom_prop_type("string"));
355 assert!(is_valid_custom_prop_type("str"));
356 assert!(is_valid_custom_prop_type("number"));
357 assert!(is_valid_custom_prop_type("float"));
358 assert!(is_valid_custom_prop_type("bool"));
359 assert!(is_valid_custom_prop_type("boolean"));
360 assert!(!is_valid_custom_prop_type("bytes"));
361 assert!(!is_valid_custom_prop_type(""));
362
363 assert!(validate_custom_prop_type("string", &PropValue::Str("hi".into())).is_ok());
365 assert!(validate_custom_prop_type("string", &PropValue::Int(1)).is_err());
366 assert!(validate_custom_prop_type("int", &PropValue::Int(1)).is_ok());
367 assert!(validate_custom_prop_type("int", &PropValue::Num(1.5)).is_err());
368 assert!(validate_custom_prop_type("number", &PropValue::Num(1.5)).is_ok());
369 assert!(validate_custom_prop_type("number", &PropValue::Int(1)).is_err());
370 assert!(validate_custom_prop_type("bool", &PropValue::Bool(true)).is_ok());
371 assert!(validate_custom_prop_type("vec2", &PropValue::Vec2(1.0, 2.0)).is_ok());
372 assert!(validate_custom_prop_type("color", &PropValue::Color(1, 2, 3, 4)).is_ok());
373
374 let err = validate_custom_prop_type("bytes", &PropValue::Str("".into())).unwrap_err();
376 assert!(err.contains("unknown custom prop type"), "got {err:?}");
377 assert!(
378 err.contains("string, int, number, bool, vec2, color"),
379 "got {err:?}"
380 );
381 }
382
383 #[test]
384 fn every_schema_has_description_and_every_prop_has_doc() {
385 for schema in crate::NODE_SCHEMAS {
386 assert!(
387 !schema.description.is_empty(),
388 "{} has no description",
389 schema.type_name
390 );
391 for prop in schema.props {
392 assert!(
393 !prop.doc.is_empty(),
394 "{}::{} has no doc",
395 schema.type_name,
396 prop.key
397 );
398 assert!(
399 !prop.label.is_empty(),
400 "{}::{} has no label",
401 schema.type_name,
402 prop.key
403 );
404 }
405 }
406 }
407}