//! Vector-style room painting. //! //! In real AGI a PICTURE resource is a stream of drawing opcodes. MRPGI keeps //! that spirit but in a clean, hand-editable form: a room is just a list of //! [`PicOp`]s that paint into the visual and/or priority buffers. Leaving a //! buffer as `None` lets you, say, set depth without drawing anything visible. use crate::framebuffer::{band, FrameBuffer, PIC_H, PIC_W}; use crate::view::{Cel, TRANSPARENT}; #[derive(Clone)] pub enum PicOp { /// Filled rectangle. Rect { x: i32, y: i32, w: i32, h: i32, vis: Option, pri: Option, }, /// Connected polyline through the given points. Line { points: Vec<(i32, i32)>, vis: u8, pri: Option, }, /// Single pixel. Pixel { x: i32, y: i32, vis: Option, pri: Option, }, } pub fn draw(fb: &mut FrameBuffer, ops: &[PicOp]) { for op in ops { match op { PicOp::Rect { x, y, w, h, vis, pri } => { for yy in *y..(*y + *h) { for xx in *x..(*x + *w) { put(fb, xx, yy, *vis, *pri); } } } PicOp::Pixel { x, y, vis, pri } => put(fb, *x, *y, *vis, *pri), PicOp::Line { points, vis, pri } => { for pair in points.windows(2) { line(fb, pair[0], pair[1], *vis, *pri); } } } } } #[inline] fn put(fb: &mut FrameBuffer, x: i32, y: i32, vis: Option, pri: Option) { if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H { return; } let i = y as usize * PIC_W + x as usize; if let Some(c) = vis { fb.visual[i] = c; } if let Some(p) = pri { fb.priority[i] = p; } } /// Bresenham line into the visual buffer (and priority, if given). fn line(fb: &mut FrameBuffer, a: (i32, i32), b: (i32, i32), vis: u8, pri: Option) { let (mut x0, mut y0) = a; let (x1, y1) = b; let dx = (x1 - x0).abs(); let dy = -(y1 - y0).abs(); let sx = if x0 < x1 { 1 } else { -1 }; let sy = if y0 < y1 { 1 } else { -1 }; let mut err = dx + dy; loop { put(fb, x0, y0, Some(vis), pri); if x0 == x1 && y0 == y1 { break; } let e2 = 2 * err; if e2 >= dy { err += dy; x0 += sx; } if e2 <= dx { err += dx; y0 += sy; } } } /// How an image maps into a region when its size doesn't match. #[derive(Clone, Copy)] pub enum Fit { /// Scale to fill the region exactly (nearest-neighbor, keeps pixels crisp). Stretch, /// Repeat the image across the region. Tile, /// Place at original size, centered, clipped to the region. Center, } /// Stamp a cel (a quantized image or sprite) into the buffers with its /// top-left at (dx, dy). Transparent pixels are skipped; `pri` optionally /// bakes depth/control under the painted pixels (AGI's `add.to.pic`). pub fn stamp_cel(fb: &mut FrameBuffer, cel: &Cel, dx: i32, dy: i32, pri: Option) { for cy in 0..cel.h { for cx in 0..cel.w { let c = cel.at(cx, cy); if c == TRANSPARENT { continue; } put(fb, dx + cx as i32, dy + cy as i32, Some(c), pri); } } } /// The "draw a shape, pour an image into it" primitive: fill a rectangular /// region with an image using a [`Fit`] mode. The region is the shape (and can /// also stamp depth via `pri`); the image supplies the look. pub fn stamp_cel_fit( fb: &mut FrameBuffer, region: (i32, i32, i32, i32), cel: &Cel, fit: Fit, pri: Option, ) { let (rx, ry, rw, rh) = region; if rw <= 0 || rh <= 0 || cel.w == 0 || cel.h == 0 { return; } for dyk in 0..rh { for dxk in 0..rw { let (sx, sy) = match fit { Fit::Stretch => ( (dxk * cel.w as i32 / rw) as usize, (dyk * cel.h as i32 / rh) as usize, ), Fit::Tile => ((dxk as usize) % cel.w, (dyk as usize) % cel.h), Fit::Center => { let ox = (rw - cel.w as i32) / 2; let oy = (rh - cel.h as i32) / 2; let sx = dxk - ox; let sy = dyk - oy; if sx < 0 || sy < 0 || sx >= cel.w as i32 || sy >= cel.h as i32 { continue; } (sx as usize, sy as usize) } }; let c = cel.at(sx.min(cel.w - 1), sy.min(cel.h - 1)); if c == TRANSPARENT { continue; } put(fb, rx + dxk, ry + dyk, Some(c), pri); } } } // --------------------------------------------------------------------------- // Editor primitives — painting into the LOOK (visual) and MEANING (priority) // buffers. `PriFill` is how a paint stroke writes the priority/control layer: // leave it (Keep), stamp a fixed control value (Set), or restore the default // walkable depth band for that row (Band). // --------------------------------------------------------------------------- #[derive(Clone, Copy)] pub enum PriFill { Keep, Set(u8), Band, } #[inline] fn put_pri(fb: &mut FrameBuffer, x: i32, y: i32, p: PriFill) { if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H { return; } let i = y as usize * PIC_W + x as usize; match p { PriFill::Keep => {} PriFill::Set(v) => fb.priority[i] = v, PriFill::Band => fb.priority[i] = band(y as usize), } } /// Paint one pixel: optional visual colour + a priority write. pub fn px(fb: &mut FrameBuffer, x: i32, y: i32, color: Option, pri: PriFill) { if let Some(c) = color { put(fb, x, y, Some(c), None); } put_pri(fb, x, y, pri); } /// Bresenham line writing visual colour (+ optional priority). pub fn line_into(fb: &mut FrameBuffer, a: (i32, i32), b: (i32, i32), color: u8, pri: PriFill) { let (mut x0, mut y0) = a; let (x1, y1) = b; let dx = (x1 - x0).abs(); let dy = -(y1 - y0).abs(); let sx = if x0 < x1 { 1 } else { -1 }; let sy = if y0 < y1 { 1 } else { -1 }; let mut err = dx + dy; loop { px(fb, x0, y0, Some(color), pri); if x0 == x1 && y0 == y1 { break; } let e2 = 2 * err; if e2 >= dy { err += dy; x0 += sx; } if e2 <= dx { err += dx; y0 += sy; } } } /// Filled rectangle into visual (+ optional priority). pub fn rect_into(fb: &mut FrameBuffer, x: i32, y: i32, w: i32, h: i32, color: Option, pri: PriFill) { for yy in y..(y + h) { for xx in x..(x + w) { px(fb, xx, yy, color, pri); } } } /// Flood fill the connected region sharing the start pixel's visual colour. /// Writes `color` to the visual buffer and `pri` to the priority buffer for /// every filled pixel. Returns the number of pixels filled (for leak checks). pub fn flood_fill(fb: &mut FrameBuffer, sx: i32, sy: i32, color: Option, pri: PriFill) -> usize { if sx < 0 || sy < 0 || sx as usize >= PIC_W || sy as usize >= PIC_H { return 0; } let target = fb.visual[sy as usize * PIC_W + sx as usize]; let mut visited = vec![false; PIC_W * PIC_H]; let mut stack = vec![(sx, sy)]; let mut count = 0usize; while let Some((x, y)) = stack.pop() { if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H { continue; } let i = y as usize * PIC_W + x as usize; if visited[i] || fb.visual[i] != target { continue; } visited[i] = true; px(fb, x, y, color, pri); count += 1; stack.push((x + 1, y)); stack.push((x - 1, y)); stack.push((x, y + 1)); stack.push((x, y - 1)); } count } /// Filled ellipse inscribed in the rect (x, y, w, h). pub fn ellipse_into(fb: &mut FrameBuffer, x: i32, y: i32, w: i32, h: i32, color: Option, pri: PriFill) { if w <= 0 || h <= 0 { return; } let rx = w as f32 / 2.0; let ry = h as f32 / 2.0; let cx = x as f32 + rx - 0.5; let cy = y as f32 + ry - 0.5; for yy in y..(y + h) { for xx in x..(x + w) { let nx = (xx as f32 - cx) / rx; let ny = (yy as f32 - cy) / ry; if nx * nx + ny * ny <= 1.0 { px(fb, xx, yy, color, pri); } } } }