diff --git a/crates/exporters/src/image_export.rs b/crates/exporters/src/image_export.rs index 95bca50..bfcc0ba 100644 --- a/crates/exporters/src/image_export.rs +++ b/crates/exporters/src/image_export.rs @@ -1,7 +1,217 @@ +use image::{ImageFormat, Rgba, RgbaImage}; use lib::{BlockPalette, StructureExporter, VoxelGrid, VoxelType}; const GAP: u32 = 1; +const COLOR_BODY: [u8; 4] = [0x4A, 0x90, 0xD9, 0xFF]; +const COLOR_OUTLINE: [u8; 4] = [0x88, 0x88, 0x88, 0xFF]; +const COLOR_SHADOW: [u8; 4] = [0xC0, 0xA0, 0x60, 0xFF]; +const COLOR_BG: [u8; 4] = [0x22, 0x22, 0x22, 0xFF]; +const COLOR_TEXT: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; + +fn voxel_color(vt: VoxelType) -> [u8; 4] { + match vt { + VoxelType::Body => COLOR_BODY, + VoxelType::Outline => COLOR_OUTLINE, + VoxelType::Shadow => COLOR_SHADOW, + } +} + +fn voxel_letter(vt: VoxelType) -> char { + match vt { + VoxelType::Body => 'B', + VoxelType::Outline => 'O', + VoxelType::Shadow => 'S', + } +} + +/// 5×7 bitmap font indexed by ASCII code. Bit4=left, Bit0=right per row. +static FONT_5X7: [[u8; 7]; 128] = { + const __: [u8; 7] = [0; 7]; + [ + // 0-31: control chars (blank) + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, + // 32 ' ' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + // 33 '!' + [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04], + // 34-47: unused (blank) + __, __, __, __, __, __, __, __, __, __, __, __, __, __, + // 48 '0' + [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], + // 49 '1' + [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E], + // 50 '2' + [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], + // 51 '3' + [0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E], + // 52 '4' + [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], + // 53 '5' + [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E], + // 54 '6' + [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E], + // 55 '7' + [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08], + // 56 '8' + [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], + // 57 '9' + [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C], + // 58 ':' + [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00], + // 59-64: blank + __, __, __, __, __, __, + // 65 'A' + [0x04, 0x0A, 0x11, 0x11, 0x1F, 0x11, 0x11], + // 66 'B' + [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E], + // 67 'C' + [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], + // 68 'D' + [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C], + // 69 'E' + [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], + // 70 'F' + [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10], + // 71 'G' + [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F], + // 72 'H' + [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], + // 73 'I' + [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], + // 74 'J' + [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C], + // 75 'K' + [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], + // 76 'L' + [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F], + // 77 'M' + [0x11, 0x1B, 0x15, 0x11, 0x11, 0x11, 0x11], + // 78 'N' + [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11], + // 79 'O' + [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], + // 80 'P' + [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10], + // 81 'Q' + [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], + // 82 'R' + [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11], + // 83 'S' + [0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E], + // 84 'T' + [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], + // 85 'U' + [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], + // 86 'V' + [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04], + // 87 'W' + [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11], + // 88 'X' + [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], + // 89 'Y' + [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], + // 90 'Z' + [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], + // 91-94: blank + __, __, __, __, + // 95 '_' + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F], + // 96: blank + __, + // 97 'a' + [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F], + // 98 'b' + [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E], + // 99 'c' + [0x00, 0x00, 0x0E, 0x10, 0x10, 0x10, 0x0E], + // 100 'd' + [0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F], + // 101 'e' + [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E], + // 102 'f' + [0x06, 0x09, 0x08, 0x1E, 0x08, 0x08, 0x08], + // 103 'g' + [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x0E], + // 104 'h' + [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x11], + // 105 'i' + [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E], + // 106 'j' + [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C], + // 107 'k' + [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12], + // 108 'l' + [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], + // 109 'm' + [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11], + // 110 'n' + [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11], + // 111 'o' + [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E], + // 112 'p' + [0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10], + // 113 'q' + [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01], + // 114 'r' + [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10], + // 115 's' + [0x00, 0x00, 0x0E, 0x10, 0x0E, 0x01, 0x0E], + // 116 't' + [0x08, 0x08, 0x1E, 0x08, 0x08, 0x09, 0x06], + // 117 'u' + [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D], + // 118 'v' + [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04], + // 119 'w' + [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A], + // 120 'x' + [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11], + // 121 'y' + [0x00, 0x11, 0x11, 0x0F, 0x01, 0x11, 0x0E], + // 122 'z' + [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F], + // 123-127: blank + __, __, __, __, __, + ] +}; + +fn draw_rect(img: &mut RgbaImage, x: u32, y: u32, w: u32, h: u32, color: [u8; 4]) { + let c = Rgba(color); + for dy in 0..h { + for dx in 0..w { + let px = x + dx; + let py = y + dy; + if px < img.width() && py < img.height() { + img.put_pixel(px, py, c); + } + } + } +} + +/// Render one char at (x, y). Each font pixel = scale×scale actual pixels. +fn draw_char(img: &mut RgbaImage, x: u32, y: u32, ch: char, scale: u32, color: [u8; 4]) { + let code = ch as usize; + if code >= 128 { return; } + let glyph = FONT_5X7[code]; + for (row, &bits) in glyph.iter().enumerate() { + for col in 0..5u32 { + if bits & (1 << (4 - col)) != 0 { + draw_rect(img, x + col * scale, y + row as u32 * scale, scale, scale, color); + } + } + } +} + +/// Render a string left-to-right. Each char is 6*scale px wide (5px + 1px gap). +fn draw_text(img: &mut RgbaImage, x: u32, y: u32, text: &str, scale: u32, color: [u8; 4]) { + let char_w = 6 * scale; + for (i, ch) in text.chars().enumerate() { + draw_char(img, x + i as u32 * char_w, y, ch, scale, color); + } +} + // 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]; @@ -93,6 +303,13 @@ mod tests { }"#).unwrap() } + #[test] + fn letter_for_each_voxel_type() { + assert_eq!(voxel_letter(VoxelType::Body), 'B'); + assert_eq!(voxel_letter(VoxelType::Outline), 'O'); + assert_eq!(voxel_letter(VoxelType::Shadow), 'S'); + } + #[test] fn dimensions_single_layer() { let grid = VoxelGrid::new(4, 3, 1);