feat(exporters): bitmap font, colors, drawing primitives

This commit is contained in:
2026-03-23 02:37:31 +01:00
parent 56c53d7f53
commit 01f64caea1

View File

@@ -1,7 +1,217 @@
use image::{ImageFormat, Rgba, RgbaImage};
use lib::{BlockPalette, StructureExporter, VoxelGrid, VoxelType}; use lib::{BlockPalette, StructureExporter, VoxelGrid, VoxelType};
const GAP: u32 = 1; 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. // Returns VoxelTypes that (1) are configured in the palette AND (2) appear in the grid.
fn active_voxel_types(palette: &BlockPalette, grid: &VoxelGrid) -> Vec<VoxelType> { fn active_voxel_types(palette: &BlockPalette, grid: &VoxelGrid) -> Vec<VoxelType> {
let mut candidates = vec![VoxelType::Body]; let mut candidates = vec![VoxelType::Body];
@@ -93,6 +303,13 @@ mod tests {
}"#).unwrap() }"#).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] #[test]
fn dimensions_single_layer() { fn dimensions_single_layer() {
let grid = VoxelGrid::new(4, 3, 1); let grid = VoxelGrid::new(4, 3, 1);