Skip to main content

mittens_engine/engine/ecs/component/
texture.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::Component;
3use crate::engine::graphics::TextureHandle;
4use std::path::Path;
5
6/// Runtime texture source/encoding understood by the engine.
7///
8/// This is intentionally *not* serialized; it is derived from `uri` when the component is
9/// created/decoded.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CatEngineTextureFormat {
12    /// Any image format decodable by the `image` crate; uploaded as RGBA8.
13    Rgba8,
14    /// DDS container containing BC7 blocks (UNorm or UNorm_sRGB).
15    DdsBc7,
16}
17
18/// Where a texture comes from.
19///
20/// For v1 GLTF support, imported textures can be represented as a namespaced URI string
21/// like "{gltf_name}:{image_name_or_index}".
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum TextureSource {
24    /// A URI-like string. Today this is typically a filesystem path (optionally `file://`),
25    /// but it can also be a virtual key (e.g. for imported textures).
26    Uri(String),
27    /// A renderer-provided handle, already uploaded.
28    Handle(TextureHandle),
29}
30
31impl CatEngineTextureFormat {
32    pub fn from_uri(uri: &str) -> Self {
33        // We currently treat `uri` as a filesystem path (optionally prefixed with `file://`).
34        let raw_path_str = uri.strip_prefix("file://").unwrap_or(uri);
35        let ext = Path::new(raw_path_str)
36            .extension()
37            .and_then(|e| e.to_str())
38            .unwrap_or("");
39
40        if ext.eq_ignore_ascii_case("dds") {
41            CatEngineTextureFormat::DdsBc7
42        } else {
43            CatEngineTextureFormat::Rgba8
44        }
45    }
46}
47
48/// Reference to a texture image by URI.
49///
50/// This component is intended to be attached as a descendant of a `RenderableComponent`.
51/// The URI is stored in `TextureSystem`; loading, decoding, and GPU upload happen when the
52/// system sees the texture is attached to a renderable.
53#[derive(Debug, Clone)]
54pub struct TextureComponent {
55    pub source: TextureSource,
56    pub format: CatEngineTextureFormat,
57    pub render_image: Option<String>,
58}
59
60impl TextureComponent {
61    pub fn new(uri: impl Into<String>) -> Self {
62        let uri = uri.into();
63        let format = CatEngineTextureFormat::from_uri(&uri);
64        Self {
65            source: TextureSource::Uri(uri),
66            format,
67            render_image: None,
68        }
69    }
70
71    pub fn unresolved() -> Self {
72        Self {
73            source: TextureSource::Uri(String::new()),
74            format: CatEngineTextureFormat::Rgba8,
75            render_image: None,
76        }
77    }
78
79    pub fn with_uri(uri: impl Into<String>) -> Self {
80        Self::new(uri)
81    }
82
83    pub fn render_image(selector: impl Into<String>) -> Self {
84        Self {
85            source: TextureSource::Uri(String::new()),
86            format: CatEngineTextureFormat::Rgba8,
87            render_image: Some(selector.into()),
88        }
89    }
90
91    pub fn from_handle(handle: TextureHandle) -> Self {
92        Self {
93            source: TextureSource::Handle(handle),
94            // Format is irrelevant for handle-based textures (already uploaded), but keep a
95            // sensible default.
96            format: CatEngineTextureFormat::Rgba8,
97            render_image: None,
98        }
99    }
100
101    /// Construct a texture component referencing a PNG file.
102    ///
103    /// Currently, the engine treats `uri` as a local filesystem path (optionally prefixed
104    /// with `file://`).
105    pub fn from_png(uri: impl Into<String>) -> Self {
106        let mut c = Self::new(uri);
107        c.format = CatEngineTextureFormat::Rgba8;
108        c
109    }
110
111    /// Construct a texture component referencing a DDS file containing BC7 blocks.
112    pub fn from_dds(uri: impl Into<String>) -> Self {
113        let mut c = Self::new(uri);
114        c.format = CatEngineTextureFormat::DdsBc7;
115        c
116    }
117
118    pub fn refresh_format_from_uri(&mut self) {
119        if let TextureSource::Uri(uri) = &self.source {
120            self.format = CatEngineTextureFormat::from_uri(uri);
121        }
122    }
123
124    pub fn uri(&self) -> Option<&str> {
125        match &self.source {
126            TextureSource::Uri(s) if !s.is_empty() => Some(s.as_str()),
127            TextureSource::Handle(_) => None,
128            TextureSource::Uri(_) => None,
129        }
130    }
131}
132
133impl Component for TextureComponent {
134    fn name(&self) -> &'static str {
135        "texture"
136    }
137
138    fn as_any(&self) -> &dyn std::any::Any {
139        self
140    }
141
142    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
143        self
144    }
145
146    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
147        emit.push_intent_now(
148            component,
149            crate::engine::ecs::IntentValue::RegisterTexture {
150                component_ids: vec![component],
151            },
152        );
153    }
154
155    fn to_mms_ast(
156        &self,
157        _world: &crate::engine::ecs::World,
158    ) -> crate::scripting::ast::ComponentExpression {
159        use crate::engine::ecs::component::ce_helpers::*;
160        if let Some(selector) = &self.render_image {
161            return ce_call("Texture", "render_image", vec![s(selector)]);
162        }
163        match (&self.source, self.format) {
164            (TextureSource::Uri(uri), _) if uri.is_empty() => ce("Texture"),
165            (TextureSource::Uri(uri), CatEngineTextureFormat::DdsBc7) => {
166                ce_call("Texture", "from_dds", vec![s(uri)])
167            }
168            (TextureSource::Uri(uri), CatEngineTextureFormat::Rgba8) => {
169                ce_call("Texture", "with_uri", vec![s(uri)])
170            }
171            // Handle-backed textures are runtime-only; emit unresolved.
172            (TextureSource::Handle(_), _) => ce("Texture"),
173        }
174    }
175}