Add Litematica exporter and integrate serde for serialization

- Introduced Litematica exporter with necessary data structures for Minecraft text generation.
- Added serde dependency for serialization support in multiple crates.
- Updated Cargo.toml files to include new dependencies and features.
- Created palette module for block palette management.
This commit is contained in:
2026-03-23 01:14:30 +01:00
parent 55d730d542
commit 1893b3427f
8 changed files with 331 additions and 0 deletions

View File

@@ -5,6 +5,9 @@ edition = "2024"
[dependencies]
lib = { path = "../lib" }
fastnbt = "2.6.1"
flate2 = "1.1.9"
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }

View File

@@ -1,3 +1,4 @@
mod litematica;
mod mcfunction;
pub use mcfunction::McFunctionExporter;

View File

@@ -0,0 +1,181 @@
use std::{
collections::HashMap,
io::Write,
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::Context;
use flate2::Compression;
use flate2::write::GzEncoder;
use lib::{StructureExporter, VoxelType};
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Litematic {
pub metadata: Metadata,
pub minecraft_data_version: i32,
pub version: i32,
pub regions: std::collections::HashMap<String, Region>,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Metadata {
pub author: String,
pub description: String,
pub enclosing_size: Vector3,
pub name: String,
pub region_count: i32,
pub time_created: i64,
pub time_modified: i64,
pub total_blocks: i32,
pub total_volume: i32,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Region {
pub block_state_palette: Vec<BlockState>,
pub block_states: Vec<i64>,
pub position: Vector3,
pub size: Vector3,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct BlockState {
pub name: String,
// Optional properties like facing=north would go here,
}
#[derive(Serialize, Clone, Copy)]
pub struct Vector3 {
pub x: i32,
pub y: i32,
pub z: i32,
}
pub struct LitematicaExporter {
palette_map: HashMap<VoxelType, String>,
}
impl LitematicaExporter {
pub fn new(body_block: &str, outline_block: &str) -> Self {
let mut palette_map = HashMap::new();
palette_map.insert(VoxelType::Body, body_block.to_string());
palette_map.insert(VoxelType::Outline, outline_block.to_string());
Self { palette_map }
}
fn pack_bits(indicies: &[usize], bits_per_entry: usize) -> Vec<i64> {
let entries_per_long = 64 / bits_per_entry;
let mut packed = Vec::new();
let mut current_long: i64 = 0;
let mut current_idx = 0;
for &index in indicies {
current_long |= (index as i64) << (current_idx * bits_per_entry);
current_idx += 1;
if current_idx >= entries_per_long {
packed.push(current_long as i64);
current_long = 0;
current_idx = 0;
}
}
if current_idx > 0 {
packed.push(current_long as i64);
}
packed
}
}
impl StructureExporter for LitematicaExporter {
fn export(&self, grid: &lib::VoxelGrid) -> anyhow::Result<Vec<u8>> {
let mut palette_list = vec![BlockState {
name: "minecraft:air".to_string(),
}];
let mut type_to_index = HashMap::new();
for (voxel_type, block_id) in &self.palette_map {
type_to_index.insert(*voxel_type, palette_list.len());
palette_list.push(BlockState {
name: block_id.clone(),
});
}
let bits_per_entry =
std::cmp::max(2, (palette_list.len() as f64 - 1.0).log2().ceil() as usize);
let mut indices = Vec::with_capacity((grid.width * grid.height * grid.depth) as usize);
let mut total_blocks = 0;
for y in 0..grid.height {
for z in 0..grid.depth {
for x in 0..grid.width {
if let Some(voxel) = grid.get(x, y, z) {
indices.push(*type_to_index.get(&voxel).unwrap());
total_blocks += 1;
} else {
indices.push(0); // Index 0 is Air
}
}
}
}
let packed_states = Self::pack_bits(&indices, bits_per_entry);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
let dimensions = Vector3 {
x: grid.width as i32,
y: grid.height as i32,
z: grid.depth as i32,
};
let mut regions = HashMap::new();
regions.insert(
"TextRegion".to_string(),
Region {
block_state_palette: palette_list,
block_states: packed_states,
position: Vector3 { x: 0, y: 0, z: 0 },
size: dimensions,
},
);
let litematic = Litematic {
minecraft_data_version: 3465, // 1.20+ Data Version
version: 6, // Litematica format version
metadata: Metadata {
author: "Minecraft Text Builder".to_string(),
description: "Generated 3D Text".to_string(),
name: "Text_Structure".to_string(),
enclosing_size: dimensions,
region_count: 1,
total_blocks,
total_volume: (grid.width * grid.height * grid.depth) as i32,
time_created: timestamp,
time_modified: timestamp,
},
regions,
};
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
let nbt_bytes = fastnbt::to_bytes(&litematic).context("NBT serialization failed")?;
encoder.write_all(&nbt_bytes)?;
Ok(encoder.finish()?)
}
fn file_extension(&self) -> &'static str {
"litematic"
}
}

View File

@@ -6,5 +6,6 @@ edition = "2024"
[dependencies]
ab_glyph = "0.2.32"
anyhow = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }

View File

@@ -4,6 +4,7 @@ mod font;
mod fonts;
mod grid;
mod models;
mod palette;
pub use engine::{GenerationOptions, TextBuilder};
pub use error::{FontError, VoxelError};

34
crates/lib/src/palette.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::VoxelType;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockPalette {
pub name: String,
pub author: Option<String>,
pub blocks: PaletteMappings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaletteMappings {
pub body: String,
pub outline: Option<String>,
pub shadow: Option<String>,
}
impl BlockPalette {
pub fn resolve(&self, voxel: &VoxelType) -> String {
match voxel {
VoxelType::Body => self.blocks.body.clone(),
VoxelType::Outline => self
.blocks
.outline
.clone()
.unwrap_or_else(|| "minecraft:air".to_string()),
VoxelType::Shadow => self
.blocks
.shadow
.clone()
.unwrap_or_else(|| "minecraft:air".to_string()),
}
}
}