mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/data/renderer.rs')
-rw-r--r--src/data/renderer.rs365
1 files changed, 323 insertions, 42 deletions
diff --git a/src/data/renderer.rs b/src/data/renderer.rs
index a1af661..0454d3c 100644
--- a/src/data/renderer.rs
+++ b/src/data/renderer.rs
@@ -2,20 +2,21 @@
use dashmap::mapref::one::Ref;
use dashmap::DashMap;
use image::codecs::png::PngDecoder;
-use image::{DynamicImage, RgbaImage};
+pub(crate) use image::{DynamicImage, RgbaImage};
use std::io::{BufReader, Cursor};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use zip::ZipArchive;
use crate::block::environment::METAL_FLOOR;
-use crate::data::map::Tile;
+use crate::block::{Block, Rotation};
use crate::team::SHARDED;
-use crate::utils::ImageUtils;
+pub(crate) use crate::utils::ImageUtils;
use crate::Map;
-pub use std::borrow::Borrow;
+pub(crate) use std::borrow::{Borrow, BorrowMut};
use super::schematic::Schematic;
+use super::GridPos;
type Cache = DashMap<PathBuf, RgbaImage>;
fn cache() -> &'static Cache {
@@ -45,6 +46,18 @@ impl Borrow<RgbaImage> for ImageHolder {
}
}
+impl BorrowMut<RgbaImage> for ImageHolder {
+ fn borrow_mut(&mut self) -> &mut RgbaImage {
+ match self {
+ Self::Own(x) => x,
+ Self::Borrow(_) => {
+ *self = Self::from(std::mem::replace(self, Self::from(RgbaImage::new(0, 0))).own());
+ self.borrow_mut()
+ }
+ }
+ }
+}
+
impl From<Option<Ref<'static, PathBuf, RgbaImage>>> for ImageHolder {
fn from(value: Option<Ref<'static, PathBuf, RgbaImage>>) -> Self {
Self::Borrow(value.unwrap())
@@ -63,6 +76,61 @@ impl From<RgbaImage> for ImageHolder {
}
}
+pub type Cross<'l> = [Option<(&'l Block, Rotation)>; 4];
+/// holds the 4 bordering blocks
+#[derive(Copy, Clone)]
+pub struct RenderingContext<'l> {
+ pub cross: Cross<'l>,
+ pub rotation: Rotation,
+ pub position: PositionContext,
+}
+
+/// holds positions
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub struct PositionContext {
+ pub position: GridPos,
+ pub width: usize,
+ pub height: usize,
+}
+
+impl std::fmt::Debug for PositionContext {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "PC<{:?} ({}/{})>",
+ self.position, self.width, self.height
+ )
+ }
+}
+
+impl std::fmt::Debug for RenderingContext<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ std::fmt::Display::fmt(self, f)
+ }
+}
+
+impl std::fmt::Display for RenderingContext<'_> {
+ /// this display impl shows RC<$directions=+own rotation>
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "RC<")?;
+ macro_rules! f {
+ ($f:expr, $z:expr, $x:literal, $at: expr, $srot: expr) => {
+ if let Some((_, rot)) = $z {
+ if (rot == $at && rot.mirrored(true, true) != $srot) {
+ $f.write_str($x)?;
+ }
+ }
+ };
+ }
+ f!(f, self.cross[0], "N = ", Rotation::Down, self.rotation);
+ f!(f, self.cross[1], "E = ", Rotation::Left, self.rotation);
+ f!(f, self.cross[2], "S = ", Rotation::Up, self.rotation);
+ f!(f, self.cross[3], "W = ", Rotation::Right, self.rotation);
+
+ write!(f, "{:?}>", self.rotation)
+ }
+}
+
static CACHE: OnceLock<Cache> = OnceLock::new();
pub(crate) fn load(category: &str, name: &str) -> Option<Ref<'static, PathBuf, RgbaImage>> {
let key = Path::new("blocks").join(category).join(name);
@@ -83,7 +151,10 @@ pub(crate) fn load(category: &str, name: &str) -> Option<Ref<'static, PathBuf, R
fn load_raw(f: impl AsRef<Path>) -> Option<RgbaImage> {
let f = std::fs::File::open(Path::new("target/out").join(f)).ok()?;
let r = PngDecoder::new(BufReader::new(f)).unwrap();
- Some(DynamicImage::from_decoder(r).unwrap().into_rgba8())
+ let p = DynamicImage::from_decoder(r).unwrap().into_rgba8();
+ assert!(p.width() != 0);
+ assert!(p.height() != 0);
+ Some(p)
}
fn load_zip() {
@@ -128,61 +199,271 @@ where
c
}
-/// renderer for creating images of schematics
-pub struct Renderer {}
-impl<'l> Renderer {
- /// creates a picture of a schematic. Bridges and node connections are not drawn, and there is no background.
- /// conveyors, conduits, and ducts currently do not render.
+pub trait RotationState {
+ fn get_rotation(&self) -> Option<Rotation>;
+}
+pub trait BlockState<'l> {
+ fn get_block(&'l self) -> Option<&'l Block>;
+}
+pub(crate) trait Crossable {
+ fn cross(&self, j: usize, c: &PositionContext) -> Cross;
+}
+
+// pub(crate) trait Darray {
+// type Output;
+// fn n(&self, j: usize, c: &PositionContext) -> Option<&Self::Output>;
+// fn e(&self, j: usize, c: &PositionContext) -> Option<&Self::Output>;
+// fn s(&self, j: usize, c: &PositionContext) -> Option<&Self::Output>;
+// fn w(&self, j: usize, c: &PositionContext) -> Option<&Self::Output>;
+// }
+
+#[cfg(test)]
+fn print_crosses(v: Vec<Cross<'_>>, height: usize) -> String {
+ let mut s = String::new();
+ for c in v.chunks(height) {
+ for c in c {
+ s.push(c[0].map_or('_', |(_, r)| r.ch()));
+ for c in c[1..].iter() {
+ s.push(',');
+ s.push(c.map_or('_', |(_, r)| r.ch()));
+ }
+ s.push(' ');
+ }
+ s.push('\n');
+ }
+ s
+}
+
+#[test]
+fn test_cross() {
+ use crate::block::distribution::define;
+ let mut reg = crate::block::BlockRegistry::default();
+ crate::block::distribution::register(&mut reg);
+ let mut ss = super::schematic::SchematicSerializer(&reg);
+ macro_rules! test {
+ ($schem: literal => $($a:tt,$b:tt,$c:tt,$d:tt)*) => {
+ let s = ss.deserialize_base64($schem).unwrap();
+ let mut c = vec![];
+ println!("{:#?}", s.blocks);
+
+ for (position, _) in s.block_iter() {
+ let pctx = PositionContext {
+ position,
+ width: s.width,
+ height: s.height,
+ };
+ c.push(s.cross(&pctx));
+ }
+ let n = s.tags.get("name").map_or("<unknown>", |x| &x);
+ let cc: Vec<Cross> = vec![
+ $(define!($a,$b,$c,$d),)*
+ ];
+ if cc != c {
+ let a = print_crosses(cc, s.height as usize);
+ let b = print_crosses(c, s.height as usize);
+ for diff in diff::lines(&a, &b) {
+ match diff {
+ diff::Result::Left(l) => println!("\x1b[38;5;1m{}", l),
+ diff::Result::Right(r) => println!("\x1b[38;5;2m{}", r),
+ diff::Result::Both(l, _) => println!("\x1b[0m{}", l),
+ }
+ }
+ print!("\x1b[0m");
+ /*
+ for diff in diff::slice(&c.into_iter().enumerate().collect::<Vec<_>>(), &cc.into_iter().enumerate().collect::<Vec<_>>()) {
+ match diff {
+ diff::Result::Left((i, l)) => println!("\x1b[38;5;1m- {l:?} at {i}"),
+ diff::Result::Right((i, r)) => println!("\x1b[38;5;2m+ {r:?} at {i}"),
+ diff::Result::Both((i, l), _) => println!("\x1b[0m {l:?} at {i}"),
+ }
+ }
+ */
+ panic!("test {n} \x1b[38;5;1mfailed\x1b[0m")
+ }
+ println!("test {n} \x1b[38;5;2mpassed\x1b[0m");
+ };
+ }
+ // crosses go from bottom left -> top left -> bottom left + 1 -> top left + 1...
+ // the symbols are directions (> => Right...), which mean the neighbors pointing direction
+ // _ = no block
+
+ // the basic test
+ // ─┐
+ // ─┤
+ test!("bXNjaAF4nGNgYmBiZmDJS8xNZWBNSizOTGbgTkktTi7KLCjJzM9jYGBgy0lMSs0pZmCNfr9gTSwjA0dyfl5ZamV+EVCOhQEBGGEEM4hiZGAGAOb+EWA=" =>
+ // (0, 0) (0, 1)
+ // n e s w borders (west void for first row)
+ >,v,_,_ _,v,>,_
+ // (1, 0) (1, 1)
+ v,_,_,> _,_,v,>
+ );
+ // the loop test
+ // ─│─
+ // ─┼┐
+ // ─└┘
+ test!("bXNjaAF4nDWK4QqAIBCDd6dE0SNGP8zuh2CeaAS9fZk0xvjGBgNjYJM7BDaqy5h3qb6EfAZNAIboNokVvKyE0Wu65NbyDhM+cQv6mTtTM/WFYfqLm6m3lx9MAg7n" =>
+ >,^,_,_ <,>,<,_ _,v,>,_
+ >,<,_,< v,v,^,> _,>,>,<
+ v,_,_,^ >,_,<,> _,_,v,v
+ );
+ // the snek test
+ // └┐
+ // ─┘
+ test!("bXNjaAF4nGNgYmBiZmDJS8xNZWApzkvNZuBOSS1OLsosKMnMz2NgYGDLSUxKzSlmYIqOZWTgSM7PK0utzC8CSrAwIAAjEIIQhGJkYAIARA0Ozg==" =>
+ ^,^,_,_ _,<,>,_
+ <,_,_,> _,_,^,^
+ );
+
+ // the notile test
+ test!("bXNjaAF4nCWJQQqAIBREx69E0Lq994oWph8STEMj6fZpzcDjDQMCSahoDsZsdN1TYB25aucz28uniMlxsdmf3wCGYDYOBbSsAqNN8eYn5XYofJEdAtSB31tfaoIVGw==" =>
+ <,>,_,_ _,^,v,_
+ ^,_,_,v _,_,>,<
+ );
+ // the asymmetrical test
+ // <───
+ // ───>
+ test!("bXNjaAF4nEXJwQqAIBAE0HGVCPrE6GC2B0HdcCPw78MKnMMwj4EFWbjiM8N5bRnLwRpqPK8oBcCU/M5JQetmMAcpNzep/cCIAfX69yv6RF0PFy0O4Q==" =>
+ <,>,_,_ _,<,>,_
+ <,>,_,> _,<,>,<
+ // <,_,_,> _,_,>,<
+ <,_,_,> _,_,>,<
+ );
+
+ // the complex test
+ // ─┬─│││─
+ // ─┤─┘─┘─
+ // ─┤┌─│─┐
+ // ─┼┘─┴─│
+ test!("bXNjaAF4nEWOUQ7CIBBEh2VZTbyCx/A2xg9a+WiC0LTGxNvb7Wjk5wEzb7M4QCO05UdBqj3PF5zuZR2XaX5OvQGwmodSV8j1FnAce3uVd1+24Iz/CYQQ8fcVHYEQIjqEXWEm9LwgX9kR+PLSbm2BMlN6Sk/3LhJnJu6S6CVmxl2MntEzv38AchUPug==" =>
+ >,v,_,_ >,v,>,_ >,v,>,_ _,v,>,_
+ v,<,_,> v,v,v,> v,>,v,> _,<,v,>
+ v,>,_,v >,<,<,v <,^,v,v _,v,>,v
+ <,>,_,< ^,v,>,v v,>,<,> _,^,^,<
+ v,<,_,> >,>,>,< ^,^,v,^ _,^,>,v
+ >,v,_,> ^,v,<,v ^,>,>,> _,>,^,^
+ // <,_,_,< ^,_,>,v v,_,<,> _,_,^,<
+ // v,_,_,> >,_,>,< ^,_,v,^ _,_,>,v
+ // >,_,_,> ^,_,<,v ^,_,>,> _,_,^,^
+ v,_,_,< >,_,v,> >,_,v,^ _,_,>,^
+ );
+}
+
+/// trait for renderable objects
+pub trait Renderable {
+ /// creates a picture of a schematic. Bridges and node connections are not drawn.
+ fn render(&self) -> RgbaImage;
+}
+
+impl Renderable for Schematic<'_> {
/// ```
/// use mindus::*;
/// let mut s = Schematic::new(2, 3);
/// s.put(0, 0, &block::distribution::DISTRIBUTOR);
- /// s.put(0, 3, &block::distribution::ROUTER);
- /// s.put(1, 3, &block::walls::COPPER_WALL);
- /// let output /*: RgbaImage */ = Renderer::render_schematic(&s);
+ /// s.put(0, 2, &block::distribution::ROUTER);
+ /// s.put(1, 2, &block::walls::COPPER_WALL);
+ /// let output /*: RgbaImage */ = s.render();
/// ```
- pub fn render_schematic(s: &'l Schematic<'_>) -> RgbaImage {
+ fn render(&self) -> RgbaImage {
+ dbg!(&self.blocks.clone());
load_zip();
- let mut canvas = RgbaImage::new((s.width * 32).into(), (s.height * 32).into());
// fill background
- canvas.repeat(METAL_FLOOR.image(None).borrow());
- for tile in s.block_iter() {
- let x = (tile.pos.0 - ((tile.block.get_size() - 1) / 2) as u16) as u32;
- let y = (s.height - tile.pos.1 - ((tile.block.get_size() / 2) + 1) as u16) as u32;
- canvas.overlay(tile.image().borrow(), x * 32, y * 32);
+ let mut bg = RgbaImage::new(
+ ((self.width + 2) * 32) as u32,
+ ((self.height + 2) * 32) as u32,
+ );
+ bg.repeat(METAL_FLOOR.image(None, None).borrow());
+ let mut canvas = RgbaImage::new(
+ ((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),
+ rotation: tile.rot,
+ position: pctx,
+ })
+ } else {
+ None
+ };
+ #[cfg(debug_assertions)]
+ println!("rendering {tile:?} ({x}, {y}) [+{}]", tile.block.get_size());
+ 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;
+ canvas.overlay(
+ tile.image(ctx.as_ref()).borrow(),
+ (x + 1) * 32,
+ (y + 1) * 32,
+ );
+ // canvas.save("tmp.png").unwrap();
}
- canvas
+ #[cfg(debug_assertions)]
+ println!("finishing up");
+ image::imageops::overlay(&mut bg, canvas.shadow(), 0, 0);
+ bg
}
+}
- pub fn render_map(m: &'l Map<'_>) -> RgbaImage {
+impl Renderable for Map<'_> {
+ fn render(&self) -> RgbaImage {
load_zip();
- let mut canvas = RgbaImage::new(m.width * 8, m.height * 8);
- const VEC: Vec<&Tile<'_>> = vec![];
- let mut layers = [VEC; 2];
- for tile in m.tiles.iter() {
- if tile.has_building() {
- layers[1].push(tile)
+ let mut floor = RgbaImage::new(self.width as u32 * 8, self.height as u32 * 8);
+ let mut top = RgbaImage::new(self.width as u32 * 8, self.height as u32 * 8);
+ for (x, y, j, tile) in self.tiles.iter().enumerate().map(|(j, t)| {
+ (
+ (j % self.width),
+ // flip y
+ (self.height - (j / self.width)) - 1,
+ j,
+ t,
+ )
+ }) {
+ if tile.build().is_none() {
+ floor.overlay(
+ // SAFETY: [`load_raw`] forces nonzero image size
+ unsafe { &tile.image(None).own().scale(8) },
+ x as u32 * 8,
+ y as u32 * 8,
+ );
} else {
- layers[0].push(tile)
- }
- }
- for tiles in layers {
- for tile in tiles {
- let s = if let Some(build) = &tile.build {
+ let s = if let Some(build) = &tile.build() {
build.block.get_size()
} else {
1
};
- let x = (tile.pos.0 - ((s - 1) / 2) as u16) as u32;
- let y = (m.height as u16 - tile.pos.1 - ((s / 2) + 1) as u16) as u32;
- canvas.overlay(
- // SAFETY: surely not 0. (tile.size can never be 0). im not sure if you can load a 0 sized image.. but you might be able to.
- unsafe { &tile.image().own().scale(tile.size() as u32 * 8) },
- x * 8,
- y * 8,
+ let x = x - ((s - 1) / 2) as usize;
+ let y = y - (s / 2) as usize;
+ let ctx = (|| {
+ let b = tile.build()?;
+ if !b.block.wants_context() {
+ return None;
+ }
+ let pctx = PositionContext {
+ position: GridPos(x, y),
+ width: self.width,
+ height: self.height,
+ };
+ let rctx = RenderingContext {
+ cross: self.cross(j, &pctx),
+ rotation: b.rotation,
+ position: pctx,
+ };
+ Some(rctx)
+ })();
+ top.overlay(
+ // SAFETY: tile.size can never be 0, and [`load_raw`] forces nonzero.
+ unsafe { &tile.image(ctx.as_ref()).own().scale(tile.size() as u32 * 8) },
+ x as u32 * 8,
+ y as u32 * 8,
);
}
}
- canvas
+ image::imageops::overlay(&mut floor, top.shadow(), 0, 0);
+ floor
}
}