diff --git a/Cargo.lock b/Cargo.lock index 051d3a4..ddd473d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -83,6 +89,18 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "cfg-if" version = "1.0.4" @@ -135,16 +153,50 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "exporters" version = "0.1.0" dependencies = [ "anyhow", + "fastnbt", + "flate2", "lib", + "serde", "thiserror", "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]] name = "heck" version = "0.5.0" @@ -169,6 +221,7 @@ version = "0.1.0" dependencies = [ "ab_glyph", "anyhow", + "serde", "thiserror", "tracing", ] @@ -207,6 +260,16 @@ dependencies = [ "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]] name = "nu-ansi-term" version = "0.50.3" @@ -278,6 +341,46 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "sharded-slab" version = "0.1.7" @@ -287,6 +390,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 3309408..096e388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ anyhow = "1.0.102" thiserror = "2.0.18" tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +serde = { version = "1.0.228", features = ["derive"] } diff --git a/crates/exporters/Cargo.toml b/crates/exporters/Cargo.toml index 1ac7aab..be7734e 100644 --- a/crates/exporters/Cargo.toml +++ b/crates/exporters/Cargo.toml @@ -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 } diff --git a/crates/exporters/src/lib.rs b/crates/exporters/src/lib.rs index 535c714..b3a1462 100644 --- a/crates/exporters/src/lib.rs +++ b/crates/exporters/src/lib.rs @@ -1,3 +1,4 @@ +mod litematica; mod mcfunction; pub use mcfunction::McFunctionExporter; diff --git a/crates/exporters/src/litematica.rs b/crates/exporters/src/litematica.rs new file mode 100644 index 0000000..0a3ac64 --- /dev/null +++ b/crates/exporters/src/litematica.rs @@ -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, +} + +#[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, + pub block_states: Vec, + 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, +} + +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 { + 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> { + 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" + } +} diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index f0491f2..371f88d 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -6,5 +6,6 @@ edition = "2024" [dependencies] ab_glyph = "0.2.32" anyhow = { workspace = true } +serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 225f70e..e9c0966 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -4,6 +4,7 @@ mod font; mod fonts; mod grid; mod models; +mod palette; pub use engine::{GenerationOptions, TextBuilder}; pub use error::{FontError, VoxelError}; diff --git a/crates/lib/src/palette.rs b/crates/lib/src/palette.rs new file mode 100644 index 0000000..6b36046 --- /dev/null +++ b/crates/lib/src/palette.rs @@ -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, + pub blocks: PaletteMappings, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaletteMappings { + pub body: String, + pub outline: Option, + pub shadow: Option, +} + +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()), + } + } +}