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

109
Cargo.lock generated
View File

@@ -18,6 +18,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@@ -83,6 +89,18 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -135,16 +153,50 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "exporters" name = "exporters"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"fastnbt",
"flate2",
"lib", "lib",
"serde",
"thiserror", "thiserror",
"tracing", "tracing",
] ]
[[package]]
name = "fastnbt"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2c4573fc9ea51f5ad19c47a16b78427b127f1c00b62808c6ab392beeb5f72c9"
dependencies = [
"byteorder",
"cesu8",
"serde",
"serde_bytes",
]
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -169,6 +221,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"anyhow", "anyhow",
"serde",
"thiserror", "thiserror",
"tracing", "tracing",
] ]
@@ -207,6 +260,16 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -278,6 +341,46 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -287,6 +390,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"

View File

@@ -7,3 +7,4 @@ anyhow = "1.0.102"
thiserror = "2.0.18" thiserror = "2.0.18"
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
serde = { version = "1.0.228", features = ["derive"] }

View File

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

View File

@@ -1,3 +1,4 @@
mod litematica;
mod mcfunction; mod mcfunction;
pub use mcfunction::McFunctionExporter; 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] [dependencies]
ab_glyph = "0.2.32" ab_glyph = "0.2.32"
anyhow = { workspace = true } anyhow = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View File

@@ -4,6 +4,7 @@ mod font;
mod fonts; mod fonts;
mod grid; mod grid;
mod models; mod models;
mod palette;
pub use engine::{GenerationOptions, TextBuilder}; pub use engine::{GenerationOptions, TextBuilder};
pub use error::{FontError, VoxelError}; 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()),
}
}
}