//! Headless compositing: room buffers + depth-sorted screen objects → RGBA //! pixels → optionally a PNG. No window required — this is what lets an LLM //! *see* the frame it just authored. use crate::framebuffer::{FrameBuffer, PIC_H, PIC_W}; use crate::palette; use crate::sprite::{Prop, ScreenObj}; use crate::view; /// The real AGI trick: draw each object bottom-up by priority, but test each /// pixel against the room's priority buffer, so a high-priority column drawn /// into the *room* still occludes a sprite standing behind it. pub fn compose(room: &FrameBuffer, ego: Option<&ScreenObj>, props: &[Prop]) -> Vec { let mut vis = room.visual.clone(); let mut draws: Vec<(u8, &view::Cel, i32, i32)> = Vec::with_capacity(props.len() + 1); if let Some(ego) = ego { draws.push((ego.priority(), ego.cel(), ego.x, ego.y)); } for p in props { draws.push((p.priority(), p.cel(), p.x, p.y)); } draws.sort_by_key(|d| d.0); for (pri, cel, bx, by) in draws { let ox = bx - cel.w as i32 / 2; let oy = by - cel.h as i32; for cy in 0..cel.h { for cx in 0..cel.w { let c = cel.at(cx, cy); if c == view::TRANSPARENT { continue; } let sx = ox + cx as i32; let sy = oy + cy as i32; if sx < 0 || sy < 0 || sx as usize >= PIC_W || sy as usize >= PIC_H { continue; } let i = sy as usize * PIC_W + sx as usize; if pri >= room.priority[i] { vis[i] = c; } } } } let mut rgba = Vec::with_capacity(PIC_W * PIC_H * 4); for &v in &vis { rgba.extend_from_slice(&palette::rgba(v)); } rgba } /// Encode raw RGBA (PIC_W x PIC_H) to PNG bytes. pub fn encode_png(rgba: &[u8]) -> Vec { let img = image::RgbaImage::from_raw(PIC_W as u32, PIC_H as u32, rgba.to_vec()) .expect("buffer is always PIC_W*PIC_H*4"); let mut out = std::io::Cursor::new(Vec::new()); img.write_to(&mut out, image::ImageOutputFormat::Png).expect("png encode of a valid image"); out.into_inner() }