Skip to main content

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}