mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/data/renderer.rs')
| -rw-r--r-- | src/data/renderer.rs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/src/data/renderer.rs b/src/data/renderer.rs new file mode 100644 index 0000000..4bbcbe7 --- /dev/null +++ b/src/data/renderer.rs @@ -0,0 +1,296 @@ +//! schematic drawing +pub(crate) use super::autotile::*; +use super::schematic::Schematic; +use super::GridPos; +use crate::block::Rotation; +pub(crate) use crate::utils::{Image, ImageHolder, ImageUtils, Overlay, Repeat}; +use crate::Map; +include!(concat!(env!("OUT_DIR"), "/full.rs")); +include!(concat!(env!("OUT_DIR"), "/quar.rs")); +include!(concat!(env!("OUT_DIR"), "/eigh.rs")); + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum Scale { + Full, + // Half, + Quarter, + Eigth, +} + +impl Scale { + #[must_use] + pub const fn px(self) -> u8 { + match self { + Self::Full => 32, + Self::Quarter => 32 / 4, + Self::Eigth => 32 / 8, + } + } +} + +impl std::ops::Mul<u32> for Scale { + type Output = u32; + fn mul(self, rhs: u32) -> u32 { + self.px() as u32 * rhs + } +} + +#[macro_export] +macro_rules! load { + ("empty", $scale:expr) => { + ImageHolder::from(match $scale { + $crate::data::renderer::Scale::Quarter => &$crate::data::renderer::quar::EMPTY, + $crate::data::renderer::Scale::Eigth => &$crate::data::renderer::eigh::EMPTY, + $crate::data::renderer::Scale::Full => &$crate::data::renderer::full::EMPTY, + }.copy()) + }; + ($name:literal, $scale:expr) => { paste::paste! { + ImageHolder::from(match $scale { + $crate::data::renderer::Scale::Quarter => &$crate::data::renderer::quar::[<$name:snake:upper>], + $crate::data::renderer::Scale::Eigth => &$crate::data::renderer::eigh::[<$name:snake:upper>], + $crate::data::renderer::Scale::Full => &$crate::data::renderer::full::[<$name:snake:upper>], + }.copy()) + } }; + ($name: literal) => { paste::paste! { + [$crate::data::renderer::full::[<$name:snake:upper>].copy(), $crate::data::renderer::quar::[<$name:snake:upper>].copy(), $crate::data::renderer::eigh::[<$name:snake:upper>].copy()] + } }; + (from $v:ident which is [$($k:literal $(|)?)+], $scale: ident) => { + $crate::data::renderer::load!($scale -> match $v { + $($k => $k,)+ + }) + }; + // turn load!(s -> match x { "v" => "y" }) into match x { "v" => load!("y", s) } + ($scale:ident -> match $v:ident { $($k:pat => $nam:literal $(,)?)+ }) => { + match $v { + $($k => $crate::data::renderer::load!($nam, $scale),)+ + #[allow(unreachable_patterns)] + n => unreachable!("{n:?}"), + } + }; + (concat $x:literal => $v:ident which is [$($k:literal $(|)?)+], $scale: ident) => { paste::paste! { + match $v { + $($k => + ImageHolder::from(match $scale { + $crate::data::renderer::Scale::Quarter => &$crate::data::renderer::quar::[<$k:snake:upper _ $x:snake:upper>], + $crate::data::renderer::Scale::Eigth => &$crate::data::renderer::eigh::[<$k:snake:upper _ $x:snake:upper>], + $crate::data::renderer::Scale::Full => &$crate::data::renderer::full::[<$k:snake:upper _ $x:snake:upper>], + }.copy()), + )+ + #[allow(unreachable_patterns)] + n => unreachable!("{n:?}"), + } + } }; +} +pub(crate) use load; + +/// trait for renderable objects +pub trait Renderable { + /// create a picture + #[must_use = "i did so much work for you"] + fn render(&self) -> Image<Vec<u8>, 3>; +} + +impl Renderable for Schematic<'_> { + /// creates a picture of a schematic. Bridges and node connections are not drawn. + /// ``` + /// use mindus::*; + /// let mut s = Schematic::new(2, 3); + /// s.put(0, 0, &block::distribution::DISTRIBUTOR); + /// s.put(0, 2, &block::distribution::ROUTER); + /// s.put(1, 2, &block::walls::COPPER_WALL); + /// let output /*: Image */ = s.render(); + /// ``` + fn render(&self) -> Image<Vec<u8>, 3> { + // fill background + // SAFETY: metal-floor is 32x32, the output is a multiple of 32 + let mut bg = unsafe { + load!("metal-floor", Scale::Full).borrow().repeated( + ((self.width + 2) * 32) as u32, + ((self.height + 2) * 32) as u32, + ) + }; + let mut canvas = Image::alloc( + ((self.width + 2) * 32) as u32, + ((self.height + 2) * 32) as u32, + ); + for (GridPos(x, y), tile) in self.block_iter() { + let ctx = if tile.block.wants_context() { + let pctx = PositionContext { + position: GridPos(x, y), + width: self.width, + height: self.height, + }; + Some(RenderingContext { + cross: self.cross(&pctx), + position: pctx, + }) + } else { + None + }; + let x = x as u32 - ((tile.block.get_size() - 1) / 2) as u32; + let y = self.height as u32 - y as u32 - ((tile.block.get_size() / 2) + 1) as u32; + unsafe { + canvas.as_mut().overlay_at( + &tile + .image( + ctx.as_ref(), + tile.get_rotation().unwrap_or(Rotation::Up), + Scale::Full, + ) + .borrow(), + (x + 1) * 32, + (y + 1) * 32, + ) + }; + } + canvas.as_mut().shadow(); + for x in 0..canvas.width() { + for y in 0..canvas.height() { + // canvas has a shadow + let p2 = unsafe { canvas.pixel(x, y) }; + let p = unsafe { bg.pixel_mut(x, y) }; + crate::utils::image::blend(p.try_into().unwrap(), p2); + } + } + bg.remove_channel() + } +} + +impl Renderable for Map<'_> { + /// Draws a map + fn render(&self) -> Image<Vec<u8>, 3> { + let scale = if self.width + self.height < 2000 { + Scale::Quarter + } else { + Scale::Eigth + }; + // todo combine these (beware of floor drawing atop buildings) (planned solution:? ptr blocks) + let mut floor: Image<_, 3> = + Image::alloc(scale * self.width as u32, scale * self.height as u32); + let mut top: Image<_, 4> = + Image::alloc(scale * self.width as u32, scale * self.height as u32); + rayon::join( + || { + for y in 0..self.height { + for x in 0..self.width { + // Map::new() allocates w*h items + let j = x + self.width * y; + let tile = unsafe { self.tiles.get_unchecked(j) }; + let y = self.height - y - 1; + // draw the floor first. + // println!("draw {tile:?} ({x}, {y})"); + unsafe { + floor.as_mut().overlay_at( + &tile.floor(scale).borrow(), + scale * x as u32, + scale * y as u32, + ) + }; + if tile.has_ore() { + unsafe { + floor.as_mut().overlay_at( + &tile.ore(scale).borrow(), + scale * x as u32, + scale * y as u32, + ) + }; + } + } + } + }, + || { + for y in 0..self.height { + for x in 0..self.width { + // Map::new() allocates w*h items + let j = x + self.width * y; + let tile = unsafe { self.tiles.get_unchecked(j) }; + let y = self.height - y - 1; + // println!("draw {tile:?} ({x}, {y})"); + if let Some(build) = tile.build() { + let s = build.block.get_size(); + let x = x + - (match s { + 1 | 2 => 0, + 3 | 4 => 1, + 5 | 6 => 2, + 7 | 8 => 3, + 9 => 4, + // SAFETY: no block too big + _ => unsafe { std::hint::unreachable_unchecked() }, + }) as usize; + let y = y + - (match s { + 1 => 0, + 2 | 3 => 1, + 4 | 5 => 2, + 6 | 7 => 3, + 8 | 9 => 4, + // SAFETY: no block too big + _ => unsafe { std::hint::unreachable_unchecked() }, + }) as usize; + let ctx = if build.block.wants_context() { + let pctx = PositionContext { + position: GridPos(x, y), + width: self.width, + height: self.height, + }; + let rctx = RenderingContext { + cross: self.cross(j, &pctx), + position: pctx, + }; + Some(rctx) + } else { + None + }; + unsafe { + top.as_mut().overlay_at( + &tile.build_image(ctx.as_ref(), scale).borrow(), + scale * x as u32, + scale * y as u32, + ) + }; + } + } + } + }, + ); + unsafe { floor.as_mut().overlay_at(&top.as_ref(), 0, 0) }; + floor + } +} + +#[test] +fn all_blocks() { + use crate::block::content::Type; + use crate::content::Content; + let reg = crate::block::build_registry(); + for t in 19..Type::WorldMessage as u16 { + let t = Type::try_from(t).unwrap(); + if matches!(t, |Type::Empty| Type::SlagCentrifuge + | Type::HeatReactor + | Type::LegacyMechPad + | Type::LegacyUnitFactory + | Type::LegacyUnitFactoryAir + | Type::LegacyUnitFactoryGround + | Type::CommandCenter) + { + continue; + } + let name = dbg!(t.get_name()); + let t = reg.get(name).unwrap(); + let _ = t.image( + None, + Some(&RenderingContext { + cross: [None; 4], + position: PositionContext { + position: GridPos(0, 0), + width: 5, + height: 5, + }, + }), + Rotation::Up, + Scale::Quarter, + ); + } +} |