concinnity_core/components/texture.rs
1// 2D texture image schema.
2
3use crate::ecs::PayloadLocator;
4use crate::ecs::asset_id::AssetId;
5use alloc::string::String;
6
7/// A 2D texture image.
8///
9/// Use the `generator` field for built-in patterns or supply a `source` file path.
10///
11/// **Built-in generators:**
12///
13/// **Choosing a room texture**: for neutral indoor spaces prefer `plaster` (cream-white) or `concrete` (grey). `brick` is reddish-orange, only use it when you explicitly want that look. `stone` (dark grey-blue) suits dungeons or medieval rooms.
14///
15/// ```rust
16/// # use concinnity_core::components::Texture;
17/// Texture {
18/// generator: "brick".into(),
19/// resolution: 512,
20/// ..Default::default()
21/// };
22/// ```
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
24#[serde(default)]
25pub struct Texture {
26 /// Asset identity; injected via `inject_name`. Not part of `args`.
27 #[serde(skip)]
28 pub asset_id: AssetId,
29 /// Procedural generator name. Empty or omitted means use `source` instead.
30 pub generator: String,
31 /// Path to the source image, relative to the project root.
32 /// Used only when `generator` is empty. A `.glb` path is allowed, use
33 /// `image_index` to pick which embedded image to use.
34 pub source: String,
35 /// When `source` points to a `.glb` file, which embedded image to import.
36 /// Ignored for regular image files.
37 pub image_index: u32,
38 /// Resolution hint for procedural generators (width = height). Defaults to
39 /// 512. Ignored for file-backed textures.
40 pub resolution: u32,
41 /// Optional ceiling on the longest edge of a file-backed image, in pixels.
42 /// `0` (the default) keeps the source resolution. When set and the source is
43 /// larger, the image is box-filtered down so its longest edge is at most this
44 /// value. Useful to keep very large source maps (4K+) from bloating the
45 /// compiled scene, which stores uncompressed pixels.
46 pub max_size: u32,
47 /// Injected at load time from the compiled blob payload.
48 #[serde(skip)]
49 pub locator: Option<PayloadLocator>,
50}
51
52impl Default for Texture {
53 fn default() -> Self {
54 Self {
55 asset_id: AssetId::default(),
56 generator: String::new(),
57 source: String::new(),
58 image_index: 0,
59 resolution: 512,
60 max_size: 0,
61 locator: None,
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn a_blank_texture_generates_at_the_default_resolution_and_is_uncapped() {
72 let t = Texture::default();
73 assert!(t.source.is_empty());
74 assert!(t.generator.is_empty());
75 assert_eq!(t.image_index, 0);
76 assert_eq!(t.resolution, 512);
77 // Zero means "no downscale cap", not a zero-sized image.
78 assert_eq!(t.max_size, 0);
79 assert!(t.locator.is_none());
80 }
81
82 #[test]
83 fn a_generated_texture_names_its_generator_instead_of_a_source() {
84 let t: Texture =
85 serde_json::from_str(r#"{"generator":"checker","resolution":128}"#).unwrap();
86 assert_eq!(t.generator, "checker");
87 assert!(t.source.is_empty());
88 assert_eq!(t.resolution, 128);
89 }
90
91 #[test]
92 fn an_imported_image_parses_and_round_trips_through_postcard() {
93 let t: Texture =
94 serde_json::from_str(r#"{"source":"bistro.fbx","image_index":7,"max_size":1024}"#)
95 .unwrap();
96 // The index picks one image out of a multi-image source archive.
97 assert_eq!(t.image_index, 7);
98
99 let bytes = postcard::to_allocvec(&t).unwrap();
100 let back: Texture = postcard::from_bytes(&bytes).unwrap();
101 assert_eq!(back.source, "bistro.fbx");
102 assert_eq!(back.image_index, 7);
103 assert_eq!(back.max_size, 1024);
104 assert_eq!(back.asset_id, AssetId::default());
105 assert!(back.locator.is_none());
106 }
107}