feat(exporters): implement full PNG grid + legend rendering

This commit is contained in:
2026-03-23 02:43:30 +01:00
parent 037dcf82a4
commit 29cfe8e801

View File

@@ -271,9 +271,83 @@ impl PngExporter {
}
}
fn draw_layer_label(img: &mut RgbaImage, y: u32, z: u32, cell_size: u32) {
let label = format!("Layer {}", z);
let scale = (cell_size / 8).max(1);
let label_height = cell_size + 4;
let text_h = 7 * scale;
let text_y = y + (label_height.saturating_sub(text_h)) / 2;
draw_text(img, GAP, text_y, &label, scale, COLOR_TEXT);
}
fn draw_legend(img: &mut RgbaImage, y_start: u32, palette: &BlockPalette, grid: &VoxelGrid, cell_size: u32) {
let lscale = (cell_size / 8).max(1);
for (i, vt) in active_voxel_types(palette, grid).iter().enumerate() {
let y = y_start + i as u32 * (cell_size + GAP);
let color = voxel_color(*vt);
draw_rect(img, GAP, y, cell_size, cell_size, color);
let lw = 5 * lscale;
let lh = 7 * lscale;
let lx = GAP + (cell_size.saturating_sub(lw)) / 2;
let ly = y + (cell_size.saturating_sub(lh)) / 2;
draw_char(img, lx, ly, voxel_letter(*vt), lscale, COLOR_TEXT);
let block_name = palette.resolve(vt);
let text_x = GAP + cell_size + GAP * 3;
let text_y = y + (cell_size.saturating_sub(7)) / 2;
draw_text(img, text_x, text_y, &block_name, 1, COLOR_TEXT);
}
}
impl StructureExporter for PngExporter {
fn export(&self, _grid: &VoxelGrid) -> anyhow::Result<Vec<u8>> {
todo!("png export not yet implemented")
fn export(&self, grid: &VoxelGrid) -> anyhow::Result<Vec<u8>> {
use std::io::Cursor;
let cs = self.cell_size;
let label_height = cs + 4;
let per_layer_height = if grid.height == 0 {
0
} else {
grid.height * (cs + GAP) - GAP
};
let layer_block_height = label_height + GAP + per_layer_height;
let (img_w, img_h) = image_dimensions(grid, &self.palette, cs);
let mut img = RgbaImage::from_pixel(img_w, img_h, Rgba(COLOR_BG));
for z in 0..grid.depth {
let layer_top_y = z * (layer_block_height + GAP);
draw_layer_label(&mut img, layer_top_y, z, cs);
let grid_top_y = layer_top_y + label_height + GAP;
for y in 0..grid.height {
let vy = grid.height - 1 - y;
for x in 0..grid.width {
let px = GAP + x * (cs + GAP);
let py = grid_top_y + y * (cs + GAP);
match grid.get(x, vy, z) {
None => draw_rect(&mut img, px, py, cs, cs, COLOR_BG),
Some(vt) => {
draw_rect(&mut img, px, py, cs, cs, voxel_color(vt));
let lscale = (cs / 8).max(1);
let lw = 5 * lscale;
let lh = 7 * lscale;
let lx = px + (cs.saturating_sub(lw)) / 2;
let ly = py + (cs.saturating_sub(lh)) / 2;
draw_char(&mut img, lx, ly, voxel_letter(vt), lscale, COLOR_TEXT);
}
}
}
}
}
let total_grid_height = grid.depth * layer_block_height
+ grid.depth.saturating_sub(1) * GAP;
draw_legend(&mut img, total_grid_height + GAP, &self.palette, grid, cs);
let mut buf = Cursor::new(Vec::new());
img.write_to(&mut buf, ImageFormat::Png)
.map_err(|e| anyhow::anyhow!("PNG encode failed: {}", e))?;
Ok(buf.into_inner())
}
fn file_extension(&self) -> &'static str {
@@ -333,4 +407,43 @@ mod tests {
let (_, h3) = image_dimensions(&g3, &palette, 16);
assert!(h3 > h1);
}
fn make_grid_with_voxels(w: u32, h: u32, d: u32) -> VoxelGrid {
let mut grid = VoxelGrid::new(w, h, d);
let _ = grid.set(0, 0, 0, VoxelType::Body);
let _ = grid.set(1, 0, 0, VoxelType::Outline);
if d > 1 { let _ = grid.set(0, 0, 1, VoxelType::Body); }
grid
}
#[test]
fn export_produces_valid_png() {
let grid = make_grid_with_voxels(3, 3, 1);
let bytes = PngExporter::new(&palette_full(), 16).export(&grid).unwrap();
assert_eq!(&bytes[..8], &[137u8, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn export_empty_grid_no_panic() {
let grid = VoxelGrid::new(5, 5, 1); // all None
let bytes = PngExporter::new(&palette_full(), 16).export(&grid).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn export_multi_layer_larger_than_single() {
let g1 = make_grid_with_voxels(4, 4, 1);
let g3 = make_grid_with_voxels(4, 4, 3);
let b1 = PngExporter::new(&palette_full(), 16).export(&g1).unwrap();
let b3 = PngExporter::new(&palette_full(), 16).export(&g3).unwrap();
assert!(b3.len() > b1.len());
}
#[test]
fn export_palette_with_no_outline() {
let mut grid = VoxelGrid::new(3, 3, 1);
let _ = grid.set(0, 0, 0, VoxelType::Outline);
let bytes = PngExporter::new(&palette_body_only(), 16).export(&grid).unwrap();
assert!(!bytes.is_empty());
}
}