From 56c53d7f532860692659ead486e7b34f40bb178e Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 23 Mar 2026 02:33:22 +0100 Subject: [PATCH] feat(exporters): implement image_dimensions with TDD --- crates/exporters/src/image_export.rs | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/exporters/src/image_export.rs b/crates/exporters/src/image_export.rs index fec1ea9..95bca50 100644 --- a/crates/exporters/src/image_export.rs +++ b/crates/exporters/src/image_export.rs @@ -1,5 +1,55 @@ use lib::{BlockPalette, StructureExporter, VoxelGrid, VoxelType}; +const GAP: u32 = 1; + +// Returns VoxelTypes that (1) are configured in the palette AND (2) appear in the grid. +fn active_voxel_types(palette: &BlockPalette, grid: &VoxelGrid) -> Vec { + let mut candidates = vec![VoxelType::Body]; + if palette.blocks.outline.is_some() { + candidates.push(VoxelType::Outline); + } + if palette.blocks.shadow.is_some() { + candidates.push(VoxelType::Shadow); + } + candidates.retain(|&vt| { + (0..grid.depth).any(|z| + (0..grid.height).any(|y| + (0..grid.width).any(|x| grid.get(x, y, z) == Some(vt)) + ) + ) + }); + candidates +} + +pub fn image_dimensions(grid: &VoxelGrid, palette: &BlockPalette, cell_size: u32) -> (u32, u32) { + let label_height = cell_size + 4; + let per_layer_height = if grid.height == 0 { + 0 + } else { + grid.height * (cell_size + GAP) - GAP + }; + let layer_block_height = label_height + GAP + per_layer_height; + let total_grid_height = grid.depth * layer_block_height + + grid.depth.saturating_sub(1) * GAP; + + let active = active_voxel_types(palette, grid); + let legend_height = if active.is_empty() { + 0 + } else { + active.len() as u32 * (cell_size + GAP) + GAP + }; + + let img_width = if grid.width == 0 { + 1 + } else { + GAP + grid.width * (cell_size + GAP) + }; + let img_height = total_grid_height + + if legend_height > 0 { GAP + legend_height } else { 0 }; + + (img_width, img_height) +} + pub struct PngExporter { palette: BlockPalette, cell_size: u32, @@ -20,3 +70,50 @@ impl StructureExporter for PngExporter { "png" } } + +#[cfg(test)] +mod tests { + use super::*; + + fn palette_full() -> BlockPalette { + serde_json::from_str(r#"{ + "name": "test", + "blocks": { + "body": "minecraft:stone", + "outline": "minecraft:cobblestone", + "shadow": "minecraft:gravel" + } + }"#).unwrap() + } + + fn palette_body_only() -> BlockPalette { + serde_json::from_str(r#"{ + "name": "test", + "blocks": { "body": "minecraft:stone" } + }"#).unwrap() + } + + #[test] + fn dimensions_single_layer() { + let grid = VoxelGrid::new(4, 3, 1); + let palette = palette_full(); + let (w, h) = image_dimensions(&grid, &palette, 16); + // width: 1 + 4*(16+1) = 69 + assert_eq!(w, 69); + // label_height = 20, per_layer = 3*17-1 = 50, layer_block = 71 + // total_grid = 71 + // legend = 3*17 + 1 = 52 (BUT: all-None grid → 0 active types → legend_height=0) + // total_h = 71 + 0 = 71 + assert_eq!(h, 71); + } + + #[test] + fn dimensions_multi_layer() { + let g1 = VoxelGrid::new(4, 3, 1); + let g3 = VoxelGrid::new(4, 3, 3); + let palette = palette_full(); + let (_, h1) = image_dimensions(&g1, &palette, 16); + let (_, h3) = image_dimensions(&g3, &palette, 16); + assert!(h3 > h1); + } +}