Skip to main content

mittens_engine/engine/ecs/system/
renderable_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::BackgroundColorComponent;
3use crate::engine::ecs::component::OverlayComponent;
4use crate::engine::ecs::component::{
5    BackgroundComponent, BoundsComponent, ColorComponent, EmissiveComponent,
6    LightQuantizationComponent, MeshComponent, OpacityComponent, RenderableComponent,
7    RendererSettingsComponent, TransparentCutoutComponent, UVComponent,
8};
9
10use crate::engine::ecs::World;
11use crate::engine::ecs::system::System;
12use crate::engine::ecs::system::TransformSystem;
13use crate::engine::graphics::bounds::Aabb;
14use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Transform};
15use crate::engine::graphics::{GpuRenderable, VisualWorld};
16use crate::engine::graphics::{MeshUploader, RenderAssets};
17use crate::engine::user_input::InputState;
18use std::collections::{HashMap, VecDeque};
19use std::sync::atomic::{AtomicUsize, Ordering};
20
21/// System that registers/updates renderables in the `VisualWorld`.
22///
23/// Contract / intent:
24/// - A `RenderableComponent` is expected to be a *descendant* of a `TransformComponent`.
25///   (In practice we attach renderables directly under a transform.)
26/// - Each `RenderableComponent` corresponds to exactly one `VisualWorld` instance.
27/// - The world-space model matrix for that instance is computed by walking up the component
28///   tree and multiplying all ancestor `TransformComponent` model matrices.
29#[derive(Debug, Default)]
30pub struct RenderableSystem {
31    renderables: Vec<ComponentId>,
32
33    /// Renderables that have been discovered/registered in ECS but not yet inserted into
34    /// VisualWorld because their GPU mesh isn't ready.
35    pending: HashMap<ComponentId, PendingRenderable>,
36
37    /// Per-vertex UV overrides for a renderable.
38    ///
39    /// Keyed by the RenderableComponent's ComponentId.
40    pending_uv: HashMap<ComponentId, Vec<[f32; 2]>>,
41
42    /// Cache of CPU meshes with baked UV overrides.
43    ///
44    /// Text rendering creates many glyphs that repeat the same UVs (same character) across many
45    /// instances. Without caching, we end up cloning/registering a new CPU mesh per glyph
46    /// instance, which breaks batching and explodes draw calls.
47    uv_mesh_cache: HashMap<UvMeshCacheKey, CpuMeshHandle>,
48
49    /// Per-instance color override for a renderable.
50    ///
51    /// Keyed by the RenderableComponent's ComponentId.
52    pending_color: HashMap<ComponentId, [f32; 4]>,
53
54    /// Per-instance opacity multiplier for a renderable.
55    ///
56    /// Keyed by the RenderableComponent's ComponentId.
57    pending_opacity: HashMap<ComponentId, PendingOpacity>,
58
59    /// Whether a renderable should be routed into the transparent cutout pass.
60    ///
61    /// Keyed by the RenderableComponent's ComponentId.
62    pending_cutout: HashMap<ComponentId, bool>,
63
64    /// Per-instance emissive/unlit override for a renderable.
65    ///
66    /// Keyed by the RenderableComponent's ComponentId.
67    pending_emissive: HashMap<ComponentId, f32>,
68
69    /// Per-renderable toon light quantization steps.
70    ///
71    /// Keyed by the RenderableComponent's ComponentId.
72    pending_quant_steps: HashMap<ComponentId, f32>,
73
74    /// NormalVisualisationComponents waiting for their subtree to be spawned.
75    ///
76    /// Populated by `register_normal_vis` during the intent phase.
77    /// Consumed in `flush_pending` where `RenderAssets` is available.
78    /// Tuple: (normal_vis_component_id, parent_renderable_id, base_mesh_handle, thickness)
79    pending_normal_vis: Vec<(ComponentId, ComponentId, CpuMeshHandle, f32)>,
80}
81
82#[derive(Debug, Clone, Copy)]
83struct PendingOpacity {
84    opacity: f32,
85    multiple_layers: bool,
86}
87
88impl Default for PendingOpacity {
89    fn default() -> Self {
90        Self {
91            opacity: 1.0,
92            multiple_layers: false,
93        }
94    }
95}
96
97#[derive(Debug, Clone, Copy)]
98struct EffectiveRenderableStyle {
99    color: [f32; 4],
100    opacity: PendingOpacity,
101    transparent_cutout: bool,
102    background: bool,
103    background_occluded_lit: bool,
104    overlay: bool,
105}
106
107impl Default for EffectiveRenderableStyle {
108    fn default() -> Self {
109        Self {
110            color: [1.0, 1.0, 1.0, 1.0],
111            opacity: PendingOpacity::default(),
112            transparent_cutout: false,
113            background: false,
114            background_occluded_lit: false,
115            overlay: false,
116        }
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121struct UvMeshCacheKey {
122    base_mesh: CpuMeshHandle,
123    /// Packed f32 bits for 4 UVs (x,y per vertex) => 8 u32s.
124    ///
125    /// This cache currently targets QUAD-like meshes (4 vertices), which is the hot path for
126    /// text glyphs.
127    uv_bits: [u32; 8],
128}
129
130#[derive(Debug, Clone)]
131struct PendingRenderable {
132    cpu_mesh: CpuMeshHandle,
133    material: MaterialHandle,
134    renderable_cid: ComponentId,
135    effective_style: EffectiveRenderableStyle,
136
137    /// Optional string-key override for the CPU mesh (resolved via `RenderAssets::imported_mesh`).
138    mesh_key: Option<String>,
139}
140
141fn clone_mesh_with_uv_overrides(
142    render_assets: &mut RenderAssets,
143    base_mesh: CpuMeshHandle,
144    uvs: &[[f32; 2]],
145) -> Option<CpuMeshHandle> {
146    let mut mesh = render_assets.cpu_mesh(base_mesh)?.clone();
147
148    for (i, v) in mesh.vertices.iter_mut().enumerate() {
149        v.uv = uvs.get(i).copied().unwrap_or([0.0, 0.0]);
150    }
151
152    Some(render_assets.register_mesh(mesh))
153}
154
155impl RenderableSystem {
156    fn uv_clone_audit_enabled() -> bool {
157        std::env::var("CAT_DEBUG_RENDERABLE_UV_CLONES")
158            .ok()
159            .map(|s| {
160                let s = s.trim().to_ascii_lowercase();
161                s == "1" || s == "true" || s == "on" || s == "yes"
162            })
163            .unwrap_or(false)
164    }
165
166    fn material_with_emissive(material: MaterialHandle, emissive_intensity: f32) -> MaterialHandle {
167        let is_emissive = emissive_intensity > 0.0;
168        match (material, is_emissive) {
169            (MaterialHandle::TOON_MESH, true) => MaterialHandle::EMISSIVE_TOON_MESH,
170            (MaterialHandle::SKINNED_TOON_MESH, true) => MaterialHandle::SKINNED_EMISSIVE_TOON_MESH,
171            (MaterialHandle::EMISSIVE_TOON_MESH, false) => MaterialHandle::TOON_MESH,
172            (MaterialHandle::SKINNED_EMISSIVE_TOON_MESH, false) => {
173                MaterialHandle::SKINNED_TOON_MESH
174            }
175            _ => material,
176        }
177    }
178
179    fn immediate_color_child(world: &World, node: ComponentId) -> Option<[f32; 4]> {
180        world.children_of(node).iter().find_map(|&ch| {
181            world
182                .get_component_by_id_as::<ColorComponent>(ch)
183                .map(|c| c.rgba)
184        })
185    }
186
187    fn immediate_opacity_child(world: &World, node: ComponentId) -> Option<PendingOpacity> {
188        world.children_of(node).iter().find_map(|&ch| {
189            world
190                .get_component_by_id_as::<OpacityComponent>(ch)
191                .map(|o| PendingOpacity {
192                    opacity: o.opacity,
193                    multiple_layers: o.multiple_layers,
194                })
195        })
196    }
197
198    fn immediate_cutout_child(world: &World, node: ComponentId) -> Option<bool> {
199        world.children_of(node).iter().find_map(|&ch| {
200            world
201                .get_component_by_id_as::<TransparentCutoutComponent>(ch)
202                .map(|c| c.enabled)
203        })
204    }
205
206    fn immediate_emissive_child(world: &World, node: ComponentId) -> Option<f32> {
207        world.children_of(node).iter().find_map(|&ch| {
208            world
209                .get_component_by_id_as::<EmissiveComponent>(ch)
210                .map(|e| e.intensity.max(0.0))
211        })
212    }
213
214    fn resolve_effective_renderable_style(
215        world: &World,
216        renderable_cid: ComponentId,
217    ) -> EffectiveRenderableStyle {
218        let mut style = EffectiveRenderableStyle::default();
219        let mut color_resolved = false;
220        let mut opacity_resolved = false;
221        let mut cutout_resolved = false;
222
223        if let Some(rgba) = Self::immediate_color_child(world, renderable_cid) {
224            style.color = rgba;
225            color_resolved = true;
226        }
227
228        if let Some(opacity) = Self::immediate_opacity_child(world, renderable_cid) {
229            style.opacity = opacity;
230            opacity_resolved = true;
231        }
232
233        if let Some(enabled) = Self::immediate_cutout_child(world, renderable_cid) {
234            style.transparent_cutout = enabled;
235            cutout_resolved = true;
236        }
237
238        let mut cur = renderable_cid;
239        while let Some(parent) = world.parent_of(cur) {
240            if !color_resolved {
241                if let Some(rgba) = Self::immediate_color_child(world, parent) {
242                    style.color = rgba;
243                    color_resolved = true;
244                }
245            }
246
247            if !opacity_resolved {
248                if let Some(opacity) = Self::immediate_opacity_child(world, parent) {
249                    style.opacity = opacity;
250                    opacity_resolved = true;
251                }
252            }
253
254            if !cutout_resolved {
255                if let Some(enabled) = Self::immediate_cutout_child(world, parent) {
256                    style.transparent_cutout = enabled;
257                    cutout_resolved = true;
258                }
259            }
260
261            if !style.background {
262                if let Some(bg) = world.get_component_by_id_as::<BackgroundComponent>(parent) {
263                    style.background = true;
264                    style.background_occluded_lit = bg.occlusion_and_lighting;
265                }
266            }
267
268            if !style.overlay
269                && world
270                    .get_component_by_id_as::<OverlayComponent>(parent)
271                    .is_some()
272            {
273                style.overlay = true;
274            }
275
276            cur = parent;
277        }
278
279        style
280    }
281
282    fn clone_mesh_with_uv_overrides_cached(
283        &mut self,
284        render_assets: &mut RenderAssets,
285        base_mesh: CpuMeshHandle,
286        uvs: &[[f32; 2]],
287    ) -> Option<CpuMeshHandle> {
288        // Fast path: cache only for 4-vertex meshes (text glyph quads).
289        let vertex_count = render_assets.cpu_mesh(base_mesh)?.vertices.len();
290        if vertex_count == 4 && uvs.len() >= 4 {
291            let mut uv_bits = [0u32; 8];
292            for i in 0..4 {
293                uv_bits[i * 2] = uvs[i][0].to_bits();
294                uv_bits[i * 2 + 1] = uvs[i][1].to_bits();
295            }
296
297            let key = UvMeshCacheKey { base_mesh, uv_bits };
298            if let Some(&cached) = self.uv_mesh_cache.get(&key) {
299                return Some(cached);
300            }
301
302            let new_mesh = clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)?;
303            self.uv_mesh_cache.insert(key, new_mesh);
304            return Some(new_mesh);
305        }
306
307        // Fallback: uncached bake for arbitrary meshes.
308        clone_mesh_with_uv_overrides(render_assets, base_mesh, uvs)
309    }
310}
311
312impl RenderableSystem {
313    fn apply_pending_emissive_updates_to_registered_renderables(
314        &mut self,
315        world: &mut World,
316        visuals: &mut VisualWorld,
317    ) {
318        let keys: Vec<ComponentId> = self.pending_emissive.keys().copied().collect();
319        for renderable_cid in keys {
320            let Some(renderable_comp) =
321                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
322            else {
323                let _ = self.pending_emissive.remove(&renderable_cid);
324                continue;
325            };
326            let Some(handle) = renderable_comp.get_handle() else {
327                continue;
328            };
329
330            let Some(emissive) = self.pending_emissive.get(&renderable_cid).copied() else {
331                continue;
332            };
333
334            let _ = visuals.update_emissive(handle, emissive);
335
336            if let Some(inst) = visuals.instance(handle) {
337                let material = Self::material_with_emissive(inst.renderable.material, emissive);
338                let _ = visuals.update_material(handle, material);
339            }
340            let _ = self.pending_emissive.remove(&renderable_cid);
341        }
342    }
343
344    fn apply_pending_quant_updates_to_registered_renderables(
345        &mut self,
346        world: &mut World,
347        visuals: &mut VisualWorld,
348    ) {
349        let keys: Vec<ComponentId> = self.pending_quant_steps.keys().copied().collect();
350        for renderable_cid in keys {
351            let Some(renderable_comp) =
352                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
353            else {
354                let _ = self.pending_quant_steps.remove(&renderable_cid);
355                continue;
356            };
357
358            let Some(handle) = renderable_comp.get_handle() else {
359                continue;
360            };
361
362            let Some(quant_steps) = self.pending_quant_steps.get(&renderable_cid).copied() else {
363                continue;
364            };
365
366            let _ = visuals.update_quant_steps(handle, quant_steps);
367            let _ = self.pending_quant_steps.remove(&renderable_cid);
368        }
369    }
370
371    fn apply_pending_color_updates_to_registered_renderables(
372        &mut self,
373        world: &mut World,
374        visuals: &mut VisualWorld,
375    ) {
376        let color_keys: Vec<ComponentId> = self.pending_color.keys().copied().collect();
377        for renderable_cid in color_keys {
378            let Some(renderable_comp) =
379                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
380            else {
381                let _ = self.pending_color.remove(&renderable_cid);
382                continue;
383            };
384            let Some(handle) = renderable_comp.get_handle() else {
385                // Still pending; will be handled by the pending flush.
386                continue;
387            };
388
389            let Some(color) = self.pending_color.get(&renderable_cid).copied() else {
390                continue;
391            };
392
393            let _ = visuals.update_color(handle, color);
394            let _ = self.pending_color.remove(&renderable_cid);
395        }
396    }
397
398    fn apply_pending_opacity_updates_to_registered_renderables(
399        &mut self,
400        world: &mut World,
401        visuals: &mut VisualWorld,
402    ) {
403        let keys: Vec<ComponentId> = self.pending_opacity.keys().copied().collect();
404        for renderable_cid in keys {
405            let Some(renderable_comp) =
406                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
407            else {
408                let _ = self.pending_opacity.remove(&renderable_cid);
409                continue;
410            };
411
412            let Some(handle) = renderable_comp.get_handle() else {
413                // Still pending; will be handled by the pending flush.
414                continue;
415            };
416
417            let Some(pending) = self.pending_opacity.get(&renderable_cid).copied() else {
418                continue;
419            };
420
421            let _ = visuals.update_opacity_state(handle, pending.opacity, pending.multiple_layers);
422            let _ = self.pending_opacity.remove(&renderable_cid);
423        }
424    }
425
426    fn apply_pending_cutout_updates_to_registered_renderables(
427        &mut self,
428        world: &mut World,
429        visuals: &mut VisualWorld,
430    ) {
431        let keys: Vec<ComponentId> = self.pending_cutout.keys().copied().collect();
432        for renderable_cid in keys {
433            let Some(renderable_comp) =
434                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
435            else {
436                let _ = self.pending_cutout.remove(&renderable_cid);
437                continue;
438            };
439
440            let Some(handle) = renderable_comp.get_handle() else {
441                // Still pending; will be handled by the pending flush.
442                continue;
443            };
444
445            let Some(enabled) = self.pending_cutout.get(&renderable_cid).copied() else {
446                continue;
447            };
448
449            let _ = visuals.update_transparent_cutout(handle, enabled);
450            let _ = self.pending_cutout.remove(&renderable_cid);
451        }
452    }
453
454    fn apply_pending_uv_updates_to_registered_renderables(
455        &mut self,
456        world: &mut World,
457        visuals: &mut VisualWorld,
458        render_assets: &mut RenderAssets,
459        uploader: &mut dyn MeshUploader,
460    ) {
461        // Apply UV updates to already-registered renderables.
462        let uv_keys: Vec<ComponentId> = self.pending_uv.keys().copied().collect();
463        for renderable_cid in uv_keys {
464            let Some(renderable_comp) =
465                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
466            else {
467                let _ = self.pending_uv.remove(&renderable_cid);
468                continue;
469            };
470            let Some(handle) = renderable_comp.get_handle() else {
471                // Still pending; will be handled by the pending flush.
472                continue;
473            };
474
475            let base_mesh = renderable_comp.renderable.mesh;
476            let material = renderable_comp.renderable.material;
477
478            let Some(uvs) = self.pending_uv.get(&renderable_cid).cloned() else {
479                continue;
480            };
481
482            let Some(new_mesh) =
483                self.clone_mesh_with_uv_overrides_cached(render_assets, base_mesh, &uvs)
484            else {
485                continue;
486            };
487
488            let mesh = match render_assets.gpu_mesh_handle(uploader, new_mesh) {
489                Ok(h) => h,
490                Err(_err) => continue,
491            };
492
493            let Some(model) = TransformSystem::world_model(world, renderable_cid) else {
494                continue;
495            };
496            let transform = Transform {
497                model,
498                matrix_world: model,
499                ..Default::default()
500            };
501
502            let gpu_r = GpuRenderable { mesh, material };
503            let _ = visuals.update(handle, gpu_r, transform);
504
505            if let Some(renderable_comp) =
506                world.get_component_by_id_as_mut::<RenderableComponent>(renderable_cid)
507            {
508                renderable_comp.renderable.mesh = new_mesh;
509            }
510
511            let _ = self.pending_uv.remove(&renderable_cid);
512        }
513    }
514
515    pub fn register_color(
516        &mut self,
517        world: &mut World,
518        _visuals: &mut VisualWorld,
519        component: ComponentId,
520    ) {
521        let Some(color_comp) = world.get_component_by_id_as::<ColorComponent>(component) else {
522            return;
523        };
524        // Find the ancestor RenderableComponent that this ColorComponent should apply to.
525        let mut cur = component;
526        let mut renderable_cid: Option<ComponentId> = None;
527        while let Some(parent) = world.parent_of(cur) {
528            if world
529                .get_component_by_id_as::<RenderableComponent>(parent)
530                .is_some()
531            {
532                renderable_cid = Some(parent);
533                break;
534            }
535            cur = parent;
536        }
537
538        // Normal case: ColorComponent is attached under a RenderableComponent.
539        if let Some(renderable_cid) = renderable_cid {
540            self.pending_color.insert(renderable_cid, color_comp.rgba);
541            return;
542        }
543
544        // Inheritance case: ColorComponent is attached above renderables (e.g., on TextComponent).
545        // Apply it to descendant renderables that do NOT have an explicit per-renderable ColorComponent.
546        let mut q = VecDeque::new();
547
548        // Style nodes (like ColorComponent) are typically attached as immediate children of a
549        // container node (e.g. TextComponent root). In that case the renderables we want to affect
550        // are descendants of the *container*, not descendants of the ColorComponent itself.
551        let start = world.parent_of(component).unwrap_or(component);
552        q.push_back(start);
553
554        while let Some(node) = q.pop_front() {
555            for &ch in world.children_of(node).iter() {
556                q.push_back(ch);
557            }
558
559            if world
560                .get_component_by_id_as::<RenderableComponent>(node)
561                .is_none()
562            {
563                continue;
564            }
565
566            // Don't clobber explicit per-renderable overrides.
567            if Self::immediate_color_child(world, node).is_some() {
568                continue;
569            }
570
571            self.pending_color.insert(node, color_comp.rgba);
572        }
573    }
574
575    pub fn register_opacity(
576        &mut self,
577        world: &mut World,
578        _visuals: &mut VisualWorld,
579        component: ComponentId,
580    ) {
581        let Some(opacity_comp) = world.get_component_by_id_as::<OpacityComponent>(component) else {
582            return;
583        };
584
585        let pending = PendingOpacity {
586            opacity: opacity_comp.opacity,
587            multiple_layers: opacity_comp.multiple_layers,
588        };
589
590        // Find the ancestor RenderableComponent that this OpacityComponent should apply to.
591        let mut cur = component;
592        let mut renderable_cid: Option<ComponentId> = None;
593        while let Some(parent) = world.parent_of(cur) {
594            if world
595                .get_component_by_id_as::<RenderableComponent>(parent)
596                .is_some()
597            {
598                renderable_cid = Some(parent);
599                break;
600            }
601            cur = parent;
602        }
603
604        // Normal case: OpacityComponent is attached under a RenderableComponent.
605        if let Some(renderable_cid) = renderable_cid {
606            self.pending_opacity.insert(renderable_cid, pending);
607            return;
608        }
609
610        // Inheritance case: OpacityComponent is attached above renderables (e.g., on TextComponent).
611        // Apply it to descendant renderables that do NOT have an explicit per-renderable OpacityComponent.
612        let mut q = VecDeque::new();
613        q.push_back(component);
614
615        while let Some(node) = q.pop_front() {
616            for &ch in world.children_of(node).iter() {
617                q.push_back(ch);
618            }
619
620            if world
621                .get_component_by_id_as::<RenderableComponent>(node)
622                .is_none()
623            {
624                continue;
625            }
626
627            // Don't clobber explicit per-renderable overrides.
628            if Self::immediate_opacity_child(world, node).is_some() {
629                continue;
630            }
631
632            self.pending_opacity.insert(node, pending);
633        }
634    }
635
636    pub fn register_transparent_cutout(
637        &mut self,
638        world: &mut World,
639        visuals: &mut VisualWorld,
640        component: ComponentId,
641    ) {
642        let Some(cutout_comp) =
643            world.get_component_by_id_as::<TransparentCutoutComponent>(component)
644        else {
645            return;
646        };
647
648        let pending = cutout_comp.enabled;
649
650        // Find the ancestor RenderableComponent that this TransparentCutoutComponent should apply to.
651        let mut cur = component;
652        let mut renderable_cid: Option<ComponentId> = None;
653        while let Some(parent) = world.parent_of(cur) {
654            if world
655                .get_component_by_id_as::<RenderableComponent>(parent)
656                .is_some()
657            {
658                renderable_cid = Some(parent);
659                break;
660            }
661            cur = parent;
662        }
663
664        // Normal case: TransparentCutoutComponent is attached under a RenderableComponent.
665        if let Some(renderable_cid) = renderable_cid {
666            self.pending_cutout.insert(renderable_cid, pending);
667
668            // If already registered, apply immediately.
669            if let Some(renderable_comp) =
670                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
671            {
672                if let Some(handle) = renderable_comp.get_handle() {
673                    let _ = visuals.update_transparent_cutout(handle, pending);
674                    let _ = self.pending_cutout.remove(&renderable_cid);
675                }
676            }
677
678            return;
679        }
680
681        // Inheritance case: TransparentCutoutComponent is attached above renderables (e.g., on TextComponent).
682        // Apply it to descendant renderables that do NOT have an explicit per-renderable TransparentCutoutComponent.
683        let mut q = VecDeque::new();
684        q.push_back(component);
685
686        while let Some(node) = q.pop_front() {
687            for &ch in world.children_of(node).iter() {
688                q.push_back(ch);
689            }
690
691            if world
692                .get_component_by_id_as::<RenderableComponent>(node)
693                .is_none()
694            {
695                continue;
696            }
697
698            // Don't clobber explicit per-renderable overrides.
699            if Self::immediate_cutout_child(world, node).is_some() {
700                continue;
701            }
702
703            self.pending_cutout.insert(node, pending);
704            if let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(node)
705            {
706                if let Some(handle) = renderable_comp.get_handle() {
707                    let _ = visuals.update_transparent_cutout(handle, pending);
708                    let _ = self.pending_cutout.remove(&node);
709                }
710            }
711        }
712    }
713
714    pub fn register_light_quantization(
715        &mut self,
716        world: &mut World,
717        visuals: &mut VisualWorld,
718        component: ComponentId,
719    ) {
720        let Some(q_comp) = world.get_component_by_id_as::<LightQuantizationComponent>(component)
721        else {
722            return;
723        };
724
725        // Find the ancestor RenderableComponent that this quantization setting should apply to.
726        let mut cur = component;
727        let mut renderable_cid: Option<ComponentId> = None;
728        while let Some(parent) = world.parent_of(cur) {
729            if world
730                .get_component_by_id_as::<RenderableComponent>(parent)
731                .is_some()
732            {
733                renderable_cid = Some(parent);
734                break;
735            }
736            cur = parent;
737        }
738
739        let Some(renderable_cid) = renderable_cid else {
740            return;
741        };
742
743        self.pending_quant_steps
744            .insert(renderable_cid, q_comp.quant_steps);
745
746        // If already registered, apply immediately.
747        if let Some(renderable_comp) =
748            world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
749        {
750            if let Some(handle) = renderable_comp.get_handle() {
751                let _ = visuals.update_quant_steps(handle, q_comp.quant_steps);
752                let _ = self.pending_quant_steps.remove(&renderable_cid);
753            }
754        }
755    }
756
757    pub fn register_emissive(
758        &mut self,
759        world: &mut World,
760        _visuals: &mut VisualWorld,
761        component: ComponentId,
762    ) {
763        let Some(emissive_comp) = world.get_component_by_id_as::<EmissiveComponent>(component)
764        else {
765            return;
766        };
767
768        let emissive = emissive_comp.intensity.max(0.0);
769
770        // Normal case: EmissiveComponent is attached directly under a RenderableComponent.
771        if let Some(parent) = world.parent_of(component) {
772            if world
773                .get_component_by_id_as::<RenderableComponent>(parent)
774                .is_some()
775            {
776                self.pending_emissive.insert(parent, emissive);
777                return;
778            }
779        }
780
781        // Inheritance case: EmissiveComponent is attached as a style node under a container
782        // (e.g. TextComponent). Apply it to descendant renderables that do NOT have an explicit
783        // per-renderable EmissiveComponent.
784        let start = world.parent_of(component).unwrap_or(component);
785        let mut q = VecDeque::new();
786        q.push_back(start);
787
788        while let Some(node) = q.pop_front() {
789            for &ch in world.children_of(node).iter() {
790                q.push_back(ch);
791            }
792
793            if world
794                .get_component_by_id_as::<RenderableComponent>(node)
795                .is_none()
796            {
797                continue;
798            }
799
800            if Self::immediate_emissive_child(world, node).is_some() {
801                continue;
802            }
803
804            self.pending_emissive.insert(node, emissive);
805        }
806    }
807
808    pub fn register_uv(
809        &mut self,
810        world: &mut World,
811        _visuals: &mut VisualWorld,
812        component: ComponentId,
813    ) {
814        let Some(uv_comp) = world.get_component_by_id_as::<UVComponent>(component) else {
815            return;
816        };
817        // Find the ancestor RenderableComponent that this UVComponent should apply to.
818        let mut cur = component;
819        let mut renderable_cid: Option<ComponentId> = None;
820        while let Some(parent) = world.parent_of(cur) {
821            if world
822                .get_component_by_id_as::<RenderableComponent>(parent)
823                .is_some()
824            {
825                renderable_cid = Some(parent);
826                break;
827            }
828            cur = parent;
829        }
830        let Some(renderable_cid) = renderable_cid else {
831            return;
832        };
833
834        // Cache until we can apply it during `flush_pending` (which has access to RenderAssets
835        // and can safely clone meshes per-renderable).
836        self.pending_uv.insert(renderable_cid, uv_comp.uvs.clone());
837    }
838
839    pub fn register_background_color(
840        &mut self,
841        world: &mut World,
842        visuals: &mut VisualWorld,
843        component: ComponentId,
844    ) {
845        if world
846            .get_component_by_id_as::<BackgroundColorComponent>(component)
847            .is_none()
848        {
849            return;
850        }
851
852        const DEFAULT: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
853        let rgba = world
854            .children_of(component)
855            .iter()
856            .find_map(|&ch| {
857                world
858                    .get_component_by_id_as::<ColorComponent>(ch)
859                    .map(|c| c.rgba)
860            })
861            .unwrap_or(DEFAULT);
862
863        // Global state: last registered wins.
864        visuals.set_clear_color(rgba);
865    }
866
867    pub fn register_renderer_settings(
868        &mut self,
869        world: &mut World,
870        visuals: &mut VisualWorld,
871        component: ComponentId,
872    ) {
873        let Some(settings) = world.get_component_by_id_as::<RendererSettingsComponent>(component)
874        else {
875            return;
876        };
877
878        // Global state: last registered wins.
879        visuals.set_renderer_msaa_mode(settings.msaa_mode());
880        visuals.set_preferred_window_size(settings.window_size);
881    }
882
883    /// Register a `NormalVisualisationComponent` for deferred spawning.
884    ///
885    /// Called from the `RegisterNormalVis` intent handler during tick (where `World` is
886    /// available but `RenderAssets` is not). Walks up to the nearest parent
887    /// `RenderableComponent`, records its `base_mesh` handle, and queues the spawn for
888    /// `flush_pending` where mesh vertex data can be read.
889    pub fn register_normal_vis(&mut self, world: &World, component: ComponentId) {
890        use crate::engine::ecs::component::{NormalVisualisationComponent, RenderableComponent};
891
892        let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(component)
893        else {
894            return;
895        };
896        let thickness = nv.thickness;
897
898        // Walk up to find the nearest ancestor RenderableComponent.
899        let mut cur = component;
900        let mut parent_renderable: Option<(ComponentId, CpuMeshHandle)> = None;
901        while let Some(p) = world.parent_of(cur) {
902            if let Some(r) = world.get_component_by_id_as::<RenderableComponent>(p) {
903                parent_renderable = Some((p, r.renderable.base_mesh));
904                break;
905            }
906            cur = p;
907        }
908
909        let Some((renderable_id, base_mesh)) = parent_renderable else {
910            return;
911        };
912
913        self.pending_normal_vis
914            .push((component, renderable_id, base_mesh, thickness));
915    }
916
917    /// Register a renderable component with this system.
918    ///
919    /// This is also where we ensure a `VisualWorld` instance exists for it.
920    pub fn register_renderable(
921        &mut self,
922        world: &mut World,
923        visuals: &mut VisualWorld,
924        component: ComponentId,
925    ) {
926        if !self.renderables.iter().any(|c| *c == component) {
927            self.renderables.push(component);
928        }
929
930        self.register_renderable_from_world(world, visuals, component);
931    }
932
933    pub fn remove_renderable(
934        &mut self,
935        world: &mut World,
936        visuals: &mut VisualWorld,
937        component: ComponentId,
938    ) {
939        self.renderables.retain(|&c| c != component);
940
941        let _ = self.pending.remove(&component);
942        let _ = self.pending_uv.remove(&component);
943        let _ = self.pending_color.remove(&component);
944        let _ = self.pending_opacity.remove(&component);
945        let _ = self.pending_emissive.remove(&component);
946        let _ = self.pending_quant_steps.remove(&component);
947
948        if let Some(r) = world.get_component_by_id_as_mut::<RenderableComponent>(component) {
949            if let Some(handle) = r.handle.take() {
950                let _ = visuals.remove(handle);
951            }
952        }
953    }
954
955    /// Register a renderable by walking the component graph in `World`.
956    pub fn register_renderable_from_world(
957        &mut self,
958        world: &mut World,
959        visuals: &mut VisualWorld,
960        component: ComponentId,
961    ) {
962        // If it's already registered in VisualWorld, nothing else to do.
963        {
964            let Some(renderable_comp) =
965                world.get_component_by_id_as::<RenderableComponent>(component)
966            else {
967                return;
968            };
969            if renderable_comp.get_handle().is_some() {
970                return;
971            }
972        }
973
974        // Defer insertion into VisualWorld until the GPU mesh exists.
975        let Some(renderable_comp) = world.get_component_by_id_as::<RenderableComponent>(component)
976        else {
977            return;
978        };
979
980        let mesh_key = world
981            .children_of(component)
982            .iter()
983            .copied()
984            .find_map(|cid| {
985                world
986                    .get_component_by_id_as::<MeshComponent>(cid)
987                    .map(|m| m.key.clone())
988            });
989
990        self.pending.insert(
991            component,
992            PendingRenderable {
993                cpu_mesh: renderable_comp.renderable.mesh,
994                material: renderable_comp.renderable.material,
995                renderable_cid: component,
996                effective_style: Self::resolve_effective_renderable_style(world, component),
997                mesh_key,
998            },
999        );
1000
1001        // Mark draw cache dirty only when we actually insert into visuals.
1002        let _ = visuals;
1003    }
1004
1005    /// Flush any pending renderables by uploading required meshes and inserting only
1006    /// GPU-ready instances into `VisualWorld`.
1007    pub fn flush_pending(
1008        &mut self,
1009        world: &mut World,
1010        visuals: &mut VisualWorld,
1011        render_assets: &mut RenderAssets,
1012        uploader: &mut dyn MeshUploader,
1013        queue: &mut crate::engine::ecs::CommandQueue,
1014    ) -> bool {
1015        let parse_bool_env = |name: &str| {
1016            std::env::var(name)
1017                .ok()
1018                .map(|s| {
1019                    let s = s.trim().to_ascii_lowercase();
1020                    s == "1" || s == "true" || s == "on" || s == "yes"
1021                })
1022                .unwrap_or(false)
1023        };
1024
1025        let debug_mesh_stats = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS");
1026        let debug_mesh_stats_all = parse_bool_env("CAT_DEBUG_RENDERABLE_MESH_STATS_ALL");
1027        static MESH_STATS_LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
1028        let mut inserted_any = false;
1029
1030        // println!(
1031        //     "[RenderableSystem] flush_pending: pending_len={} visuals.instances={} ",
1032        //     self.pending.len(),
1033        //     visuals.instances().len()
1034        // );
1035        // Collect keys first to avoid borrow issues.
1036        let keys: Vec<ComponentId> = self.pending.keys().copied().collect();
1037        for key in keys {
1038            let Some(p) = self.pending.get(&key).cloned() else {
1039                continue;
1040            };
1041            let effective_style = Self::resolve_effective_renderable_style(world, p.renderable_cid);
1042            if let Some(pending) = self.pending.get_mut(&key) {
1043                pending.effective_style = effective_style;
1044            }
1045
1046            let mut cpu_mesh = p.cpu_mesh;
1047
1048            // If a MeshComponent override exists, don't flush until the imported mesh resolves.
1049            if let Some(mesh_key) = p.mesh_key.as_deref() {
1050                let Some(imported) = render_assets.imported_mesh(mesh_key) else {
1051                    continue;
1052                };
1053                cpu_mesh = imported;
1054                if let Some(pending) = self.pending.get_mut(&key) {
1055                    pending.cpu_mesh = cpu_mesh;
1056                }
1057                if let Some(renderable_comp) =
1058                    world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1059                {
1060                    renderable_comp.renderable.mesh = cpu_mesh;
1061                    renderable_comp.renderable.base_mesh = cpu_mesh;
1062                }
1063            }
1064
1065            if let Some(uvs) = self.pending_uv.get(&p.renderable_cid).cloned() {
1066                if let Some(new_mesh) =
1067                    self.clone_mesh_with_uv_overrides_cached(render_assets, cpu_mesh, &uvs)
1068                {
1069                    let uv_base_mesh = cpu_mesh;
1070                    if Self::uv_clone_audit_enabled() {
1071                        let (verts, indices) = render_assets
1072                            .cpu_mesh(new_mesh)
1073                            .map(|m| (m.vertices.len(), m.indices_u32.len()))
1074                            .unwrap_or((0, 0));
1075                        println!(
1076                            "[RenderableSystem][audit] uv_clone renderable={:?} base_mesh={:?} new_mesh={:?} verts={} indices={} repeated_work=true",
1077                            p.renderable_cid, uv_base_mesh, new_mesh, verts, indices
1078                        );
1079                    }
1080                    cpu_mesh = new_mesh;
1081                    if let Some(pending) = self.pending.get_mut(&key) {
1082                        pending.cpu_mesh = cpu_mesh;
1083                    }
1084                    if let Some(renderable_comp) =
1085                        world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1086                    {
1087                        renderable_comp.renderable.mesh = cpu_mesh;
1088                        renderable_comp.renderable.base_mesh = uv_base_mesh;
1089                    }
1090                }
1091            }
1092
1093            cache_resolved_mesh_bounds(world, render_assets, p.renderable_cid, cpu_mesh);
1094
1095            // Upload/resolve GPU mesh.
1096            let mesh = match render_assets.gpu_mesh_handle(uploader, cpu_mesh) {
1097                Ok(h) => h,
1098                Err(_err) => continue,
1099            };
1100
1101            if debug_mesh_stats {
1102                let (vcount, icount, has_skin) = render_assets
1103                    .cpu_mesh(cpu_mesh)
1104                    .map(|m| {
1105                        (
1106                            m.vertices.len(),
1107                            m.indices_u32.len(),
1108                            m.joints0.is_some() && m.weights0.is_some(),
1109                        )
1110                    })
1111                    .unwrap_or((0, 0, false));
1112
1113                let key_str = p.mesh_key.as_deref().unwrap_or("<no mesh_key>");
1114                let should_log = debug_mesh_stats_all || key_str != "<no mesh_key>" || has_skin;
1115
1116                if should_log {
1117                    let limit = std::env::var("CAT_DEBUG_RENDERABLE_MESH_STATS_LIMIT")
1118                        .ok()
1119                        .and_then(|s| s.trim().parse::<usize>().ok())
1120                        .unwrap_or(50);
1121                    let n = MESH_STATS_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
1122                    if n < limit {
1123                        println!(
1124                            "[RenderableSystem] renderable={:?} material={:?} mesh_key='{}' cpu_mesh={:?} gpu_mesh={:?} verts={} indices={} skinned_attrs={}",
1125                            p.renderable_cid,
1126                            p.material,
1127                            key_str,
1128                            cpu_mesh,
1129                            mesh,
1130                            vcount,
1131                            icount,
1132                            has_skin
1133                        );
1134                    }
1135                }
1136            }
1137
1138            let gpu_r = GpuRenderable {
1139                mesh,
1140                material: p.material,
1141            };
1142
1143            let model = match TransformSystem::world_model(world, p.renderable_cid) {
1144                Some(m) => m,
1145                None => {
1146                    self.pending.remove(&key);
1147                    continue;
1148                }
1149            };
1150
1151            let transform = Transform {
1152                model,
1153                matrix_world: model,
1154                ..Default::default()
1155            };
1156
1157            let color = self
1158                .pending_color
1159                .get(&p.renderable_cid)
1160                .copied()
1161                .unwrap_or(effective_style.color);
1162
1163            let opacity = self
1164                .pending_opacity
1165                .get(&p.renderable_cid)
1166                .copied()
1167                .unwrap_or(effective_style.opacity);
1168
1169            let transparent_cutout = self
1170                .pending_cutout
1171                .get(&p.renderable_cid)
1172                .copied()
1173                .unwrap_or(effective_style.transparent_cutout);
1174
1175            let emissive = self
1176                .pending_emissive
1177                .get(&p.renderable_cid)
1178                .copied()
1179                .unwrap_or(0.0);
1180
1181            let gpu_r = GpuRenderable::new(
1182                gpu_r.mesh,
1183                Self::material_with_emissive(gpu_r.material, emissive),
1184            );
1185
1186            let quant_steps = self
1187                .pending_quant_steps
1188                .get(&p.renderable_cid)
1189                .copied()
1190                .unwrap_or_else(|| match p.material {
1191                    MaterialHandle::TOON_MESH => 3.0,
1192                    MaterialHandle::UNLIT_MESH => 1.0,
1193                    _ => 3.0,
1194                });
1195
1196            let handle = visuals.register(
1197                p.renderable_cid,
1198                gpu_r,
1199                transform,
1200                color,
1201                opacity.opacity,
1202                opacity.multiple_layers,
1203                transparent_cutout,
1204                effective_style.background,
1205                effective_style.background_occluded_lit,
1206                effective_style.overlay,
1207                emissive,
1208                None,
1209                quant_steps,
1210            );
1211            if let Some(renderable_comp) =
1212                world.get_component_by_id_as_mut::<RenderableComponent>(p.renderable_cid)
1213            {
1214                renderable_comp.handle = Some(handle);
1215            }
1216
1217            // UVs have now been baked into the mesh, if present.
1218            let _ = self.pending_uv.remove(&p.renderable_cid);
1219
1220            // Color has now been applied.
1221            let _ = self.pending_color.remove(&p.renderable_cid);
1222
1223            // Opacity has now been applied.
1224            let _ = self.pending_opacity.remove(&p.renderable_cid);
1225
1226            // Cutout has now been applied.
1227            let _ = self.pending_cutout.remove(&p.renderable_cid);
1228
1229            // Emissive has now been applied.
1230            let _ = self.pending_emissive.remove(&p.renderable_cid);
1231
1232            // Quant steps have now been applied.
1233            let _ = self.pending_quant_steps.remove(&p.renderable_cid);
1234
1235            // (If you log ComponentId in a format string, use {:?}.)
1236            self.pending.remove(&key);
1237            inserted_any = true;
1238        }
1239
1240        self.apply_pending_uv_updates_to_registered_renderables(
1241            world,
1242            visuals,
1243            render_assets,
1244            uploader,
1245        );
1246        self.apply_pending_color_updates_to_registered_renderables(world, visuals);
1247        self.apply_pending_opacity_updates_to_registered_renderables(world, visuals);
1248        self.apply_pending_cutout_updates_to_registered_renderables(world, visuals);
1249        self.apply_pending_emissive_updates_to_registered_renderables(world, visuals);
1250        self.apply_pending_quant_updates_to_registered_renderables(world, visuals);
1251
1252        self.spawn_pending_normal_vis(world, render_assets, queue);
1253
1254        inserted_any
1255    }
1256
1257    fn spawn_pending_normal_vis(
1258        &mut self,
1259        world: &mut World,
1260        render_assets: &RenderAssets,
1261        queue: &mut crate::engine::ecs::CommandQueue,
1262    ) {
1263        use crate::engine::ecs::component::{
1264            ColorComponent, EmissiveComponent, NormalVisualisationComponent, RenderableComponent,
1265            TransformComponent,
1266        };
1267        use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Renderable};
1268
1269        let pending = std::mem::take(&mut self.pending_normal_vis);
1270        for (nv_id, _renderable_id, base_mesh, thickness) in pending {
1271            // Skip if already spawned (double-init guard).
1272            if let Some(nv) = world.get_component_by_id_as::<NormalVisualisationComponent>(nv_id) {
1273                if !nv.spawned_roots.is_empty() {
1274                    continue;
1275                }
1276            } else {
1277                continue;
1278            }
1279
1280            let Some(cpu_mesh) = render_assets.cpu_mesh(base_mesh) else {
1281                // Mesh not loaded yet — try again next frame.
1282                self.pending_normal_vis
1283                    .push((nv_id, _renderable_id, base_mesh, thickness));
1284                continue;
1285            };
1286
1287            let half_height = thickness * 5.0;
1288            let mut spawned_roots: Vec<ComponentId> = Vec::new();
1289
1290            for vertex in &cpu_mesh.vertices {
1291                let pos = vertex.pos;
1292                let n = vertex.normal;
1293
1294                // Normalize the normal (defensive).
1295                let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1296                let n = if len > 1e-6 {
1297                    [n[0] / len, n[1] / len, n[2] / len]
1298                } else {
1299                    [0.0, 1.0, 0.0]
1300                };
1301
1302                // Cube center: offset half-height along the normal from the vertex.
1303                let cx = pos[0] + n[0] * half_height;
1304                let cy = pos[1] + n[1] * half_height;
1305                let cz = pos[2] + n[2] * half_height;
1306
1307                // Quaternion to rotate Y-axis [0,1,0] onto the normal.
1308                let quat = crate::utils::math::shortest_arc_quat([0.0, 1.0, 0.0], n);
1309
1310                let t_id = world.add_component(
1311                    TransformComponent::new()
1312                        .with_position(cx, cy, cz)
1313                        .with_rotation_quat(quat)
1314                        .with_scale(thickness, thickness * 10.0, thickness),
1315                );
1316                let r_id = world.add_component(RenderableComponent::new(Renderable::new(
1317                    CpuMeshHandle::CUBE,
1318                    MaterialHandle::TOON_MESH,
1319                )));
1320                let c_id = world.add_component(ColorComponent::rgba(0.0, 1.0, 1.0, 1.0));
1321                let e_id = world.add_component(EmissiveComponent::on());
1322
1323                let _ = world.add_child(nv_id, t_id);
1324                let _ = world.add_child(t_id, r_id);
1325                let _ = world.add_child(r_id, c_id);
1326                let _ = world.add_child(r_id, e_id);
1327
1328                world.init_component_tree(t_id, queue);
1329                spawned_roots.push(t_id);
1330            }
1331
1332            if let Some(nv) =
1333                world.get_component_by_id_as_mut::<NormalVisualisationComponent>(nv_id)
1334            {
1335                nv.spawned_roots = spawned_roots;
1336            }
1337        }
1338    }
1339}
1340
1341fn cache_resolved_mesh_bounds(
1342    world: &mut World,
1343    render_assets: &RenderAssets,
1344    renderable: ComponentId,
1345    mesh: CpuMeshHandle,
1346) {
1347    let Some(local) = render_assets.cpu_mesh(mesh).and_then(|cpu_mesh| {
1348        let positions: Vec<[f32; 3]> = cpu_mesh.vertices.iter().map(|vertex| vertex.pos).collect();
1349        Aabb::from_points(&positions)
1350    }) else {
1351        return;
1352    };
1353
1354    let existing_bounds = world
1355        .children_of(renderable)
1356        .iter()
1357        .copied()
1358        .find(|&child| {
1359            world
1360                .get_component_by_id_as::<BoundsComponent>(child)
1361                .is_some()
1362        });
1363    if let Some(bounds) =
1364        existing_bounds.and_then(|child| world.get_component_by_id_as_mut::<BoundsComponent>(child))
1365    {
1366        bounds.local = local;
1367        return;
1368    }
1369
1370    let bounds = world.add_component(BoundsComponent::new(local));
1371    let _ = world.add_child(renderable, bounds);
1372}
1373
1374impl System for RenderableSystem {
1375    fn tick(
1376        &mut self,
1377        _world: &mut World,
1378        _visuals: &mut VisualWorld,
1379        _input: &InputState,
1380        _dt_sec: f32,
1381    ) {
1382        // Intentionally a no-op for now.
1383        //
1384        // Per your architecture: VisualWorld registration happens at component registration time
1385        // (RenderableComponent::init -> SystemWorld::register_renderable -> RenderableSystem::register_renderable).
1386        //
1387        // Later, tick() can be used for per-frame sync (transform updates, material changes, etc.)
1388        // once we decide how to represent those components and what events/dirty flags we have.
1389    }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394    use super::RenderableSystem;
1395    use crate::engine::ecs::CommandQueue;
1396    use crate::engine::ecs::World;
1397    use crate::engine::ecs::component::{
1398        BackgroundComponent, ColorComponent, EmissiveComponent, OpacityComponent, OverlayComponent,
1399        RenderableComponent, TextComponent, TransformComponent, TransparentCutoutComponent,
1400    };
1401    use crate::engine::graphics::primitives::MeshHandle;
1402    use crate::engine::graphics::{CpuMesh, MeshUploader, RenderAssets, VisualWorld};
1403
1404    #[derive(Default)]
1405    struct TestUploader {
1406        next_mesh: u32,
1407    }
1408
1409    impl MeshUploader for TestUploader {
1410        fn upload_mesh(
1411            &mut self,
1412            _mesh: &CpuMesh,
1413        ) -> Result<MeshHandle, Box<dyn std::error::Error>> {
1414            let handle = MeshHandle(self.next_mesh);
1415            self.next_mesh += 1;
1416            Ok(handle)
1417        }
1418    }
1419
1420    #[test]
1421    fn effective_style_preserves_renderable_local_and_ancestor_semantics() {
1422        let mut world = World::default();
1423
1424        let text = world.add_component(TextComponent::new("item"));
1425        let text_color = world.add_component(ColorComponent::rgba(0.2, 0.3, 0.4, 1.0));
1426        let text_opacity = world.add_component(OpacityComponent::new().with_opacity(0.4));
1427        let overlay = world.add_component(OverlayComponent::new());
1428        let background =
1429            world.add_component(BackgroundComponent::new().with_occlusion_and_lighting());
1430        let overlay_host = world.add_component(TransformComponent::new());
1431        let glyph_t = world.add_component(TransformComponent::new());
1432        let glyph_r = world.add_component(RenderableComponent::square());
1433        let local_color = world.add_component(ColorComponent::rgba(0.9, 0.8, 0.7, 1.0));
1434        let cutout = world.add_component(TransparentCutoutComponent::new());
1435
1436        let _ = world.add_child(text, text_color);
1437        let _ = world.add_child(text, text_opacity);
1438        let _ = world.add_child(text, background);
1439        let _ = world.add_child(background, overlay);
1440        let _ = world.add_child(overlay, overlay_host);
1441        let _ = world.add_child(overlay_host, glyph_t);
1442        let _ = world.add_child(glyph_t, glyph_r);
1443        let _ = world.add_child(glyph_r, local_color);
1444        let _ = world.add_child(glyph_r, cutout);
1445
1446        let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
1447
1448        assert_eq!(style.color, [0.9, 0.8, 0.7, 1.0]);
1449        assert_eq!(style.opacity.opacity, 0.4);
1450        assert!(!style.opacity.multiple_layers);
1451        assert!(style.transparent_cutout);
1452        assert!(style.background);
1453        assert!(style.background_occluded_lit);
1454        assert!(style.overlay);
1455    }
1456
1457    #[test]
1458    fn effective_style_defaults_when_no_style_ancestors_exist() {
1459        let mut world = World::default();
1460
1461        let text = world.add_component(TextComponent::new("item"));
1462        let glyph_t = world.add_component(TransformComponent::new());
1463        let glyph_r = world.add_component(RenderableComponent::square());
1464
1465        let _ = world.add_child(text, glyph_t);
1466        let _ = world.add_child(glyph_t, glyph_r);
1467
1468        let style = RenderableSystem::resolve_effective_renderable_style(&world, glyph_r);
1469
1470        assert_eq!(style.color, [1.0, 1.0, 1.0, 1.0]);
1471        assert_eq!(style.opacity.opacity, 1.0);
1472        assert!(!style.opacity.multiple_layers);
1473        assert!(!style.transparent_cutout);
1474        assert!(!style.background);
1475        assert!(!style.background_occluded_lit);
1476        assert!(!style.overlay);
1477    }
1478
1479    #[test]
1480    fn flush_pending_recomputes_style_after_attach() {
1481        let mut world = World::default();
1482        let mut visuals = VisualWorld::default();
1483        let mut renderable_system = RenderableSystem::default();
1484        let mut render_assets = RenderAssets::new();
1485        let mut uploader = TestUploader::default();
1486        let mut queue = CommandQueue::new();
1487
1488        let overlay_root = world.add_component(OverlayComponent::new());
1489        let item_root = world.add_component(TransformComponent::new());
1490        let renderable_root = world.add_component(TransformComponent::new());
1491        let renderable = world.add_component(RenderableComponent::square());
1492
1493        let _ = world.add_child(item_root, renderable_root);
1494        let _ = world.add_child(renderable_root, renderable);
1495
1496        renderable_system.register_renderable_from_world(&mut world, &mut visuals, renderable);
1497
1498        let _ = world.add_child(overlay_root, item_root);
1499
1500        let inserted = renderable_system.flush_pending(
1501            &mut world,
1502            &mut visuals,
1503            &mut render_assets,
1504            &mut uploader,
1505            &mut queue,
1506        );
1507
1508        assert!(inserted);
1509        let handle = world
1510            .get_component_by_id_as::<RenderableComponent>(renderable)
1511            .and_then(|r| r.get_handle())
1512            .expect("renderable handle after flush");
1513        let instance = visuals.instance(handle).expect("visual instance");
1514        assert!(instance.overlay);
1515    }
1516
1517    #[test]
1518    fn text_style_emissive_targets_descendants_not_ancestor_renderable() {
1519        let mut world = World::default();
1520        let mut visuals = VisualWorld::default();
1521        let mut renderable = RenderableSystem::default();
1522
1523        let plane = world.add_component(RenderableComponent::square());
1524        let text = world.add_component(TextComponent::new("item"));
1525        let glyph_t = world.add_component(TransformComponent::new());
1526        let glyph_r = world.add_component(RenderableComponent::square());
1527        let emissive = world.add_component(EmissiveComponent::on());
1528
1529        let _ = world.add_child(plane, text);
1530        let _ = world.add_child(text, glyph_t);
1531        let _ = world.add_child(glyph_t, glyph_r);
1532        let _ = world.add_child(text, emissive);
1533
1534        renderable.register_emissive(&mut world, &mut visuals, emissive);
1535
1536        assert_eq!(renderable.pending_emissive.get(&plane), None);
1537        assert_eq!(renderable.pending_emissive.get(&glyph_r), Some(&1.0));
1538    }
1539}