diff --git a/.gitignore b/.gitignore index ea8c4bf..dd9702e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*.mcfunction \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ddd473d..a59c06f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "lazy_static" version = "1.5.0" @@ -255,6 +261,8 @@ dependencies = [ "clap", "exporters", "lib", + "serde", + "serde_json", "thiserror", "tracing", "tracing-subscriber", @@ -381,6 +389,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -547,3 +568,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 0f7f02c..2549b01 100644 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -17,6 +17,8 @@ lib = { path = "../lib" } exporters = { path = "../exporters" } clap = { version = "4.6.0", features = ["derive"] } anyhow = { workspace = true } +serde = { workspace = true } +serde_json = "1" thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/bin/src/cli.rs b/crates/bin/src/cli.rs index 9326365..31c6c37 100644 --- a/crates/bin/src/cli.rs +++ b/crates/bin/src/cli.rs @@ -2,26 +2,34 @@ use std::{fs, path::PathBuf}; use clap::Parser; use exporters::McFunctionExporter; -use lib::{StructureExporter, TextBuilder, TtfFont}; +use lib::{BlockPalette, StructureExporter, TextBuilder, TtfFont}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { /// The text you want to generate - #[arg(short, long)] - text: String, + #[arg(short, long, required_unless_present = "list_palettes")] + text: Option, /// Path to the .ttf font file - #[arg(short, long)] - font: PathBuf, + #[arg(short, long, required_unless_present = "list_palettes")] + font: Option, /// How many blocks deep the text should be #[arg(short, long, default_value_t = 1)] depth: u32, - /// The Minecraft block ID to use for the text body - #[arg(short, long, default_value = "minecraft:quartz_block")] - block: String, + /// Name of a built-in palette preset (from the palettes/ directory) + #[arg(short, long, conflicts_with = "palette_file")] + palette: Option, + + /// Path to a JSON palette file + #[arg(long)] + palette_file: Option, + + /// List available palette presets and exit + #[arg(long)] + list_palettes: bool, /// Output file path (without extension) #[arg(short, long, default_value = "output")] @@ -32,26 +40,70 @@ struct Cli { size: f32, } +fn palettes_dir() -> PathBuf { + std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("palettes") +} + +fn load_palette_by_name(name: &str) -> anyhow::Result { + let path = palettes_dir().join(format!("{}.json", name)); + let json = fs::read_to_string(&path) + .map_err(|_| anyhow::anyhow!("palette '{}' not found in {:?}", name, palettes_dir()))?; + Ok(serde_json::from_str(&json)?) +} + pub fn run() -> anyhow::Result<()> { let cli = Cli::parse(); - tracing::info!("loading font from: {:?}", cli.font); - let font_bytes = fs::read(&cli.font)?; + if cli.list_palettes { + let dir = palettes_dir(); + let entries = fs::read_dir(&dir) + .map_err(|_| anyhow::anyhow!("palettes directory not found at {:?}", dir))?; + println!("Available palettes:"); + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("json") { + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + println!(" {}", stem); + } + } + } + return Ok(()); + } - let font = TtfFont::from_bytes(&font_bytes, cli.size) - .map_err(|e| anyhow::anyhow!(e))?; + let text = cli.text.unwrap(); + let font_path = cli.font.unwrap(); - tracing::info!("generating voxel grid for text: '{}'", cli.text); + let palette = if let Some(path) = cli.palette_file { + let json = fs::read_to_string(&path) + .map_err(|e| anyhow::anyhow!("failed to read palette file {:?}: {}", path, e))?; + serde_json::from_str(&json)? + } else { + let name = cli.palette.as_deref().unwrap_or("default"); + load_palette_by_name(name)? + }; + + tracing::info!("using palette: {}", palette.name); + tracing::info!("loading font from: {:?}", font_path); + let font_bytes = fs::read(&font_path)?; + + let font = TtfFont::from_bytes(&font_bytes, cli.size).map_err(|e| anyhow::anyhow!(e))?; + + tracing::info!("generating voxel grid for text: '{}'", text); let builder = TextBuilder::new(&font).with_depth(cli.depth); - let grid = builder.generate(&cli.text); + let grid = builder.generate(&text); tracing::info!( "grid generated: {}x{}x{}", - grid.width, grid.height, grid.depth + grid.width, + grid.height, + grid.depth ); - let exporter = McFunctionExporter::new(&cli.block, "minecraft:obsidian"); - + let exporter = McFunctionExporter::new(&palette); let output_bytes = exporter.export(&grid)?; let mut out_path = cli.out.clone(); diff --git a/crates/exporters/src/lib.rs b/crates/exporters/src/lib.rs index b3a1462..3be9955 100644 --- a/crates/exporters/src/lib.rs +++ b/crates/exporters/src/lib.rs @@ -1,4 +1,5 @@ mod litematica; mod mcfunction; +pub use litematica::LitematicaExporter; pub use mcfunction::McFunctionExporter; diff --git a/crates/exporters/src/litematica.rs b/crates/exporters/src/litematica.rs index 0a3ac64..8a000f3 100644 --- a/crates/exporters/src/litematica.rs +++ b/crates/exporters/src/litematica.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::Context; use flate2::Compression; use flate2::write::GzEncoder; -use lib::{StructureExporter, VoxelType}; +use lib::{BlockPalette, StructureExporter, VoxelType}; use serde::Serialize; #[derive(Serialize)] @@ -61,10 +61,11 @@ pub struct LitematicaExporter { } impl LitematicaExporter { - pub fn new(body_block: &str, outline_block: &str) -> Self { + pub fn new(palette: &BlockPalette) -> 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()); + palette_map.insert(VoxelType::Body, palette.resolve(&VoxelType::Body)); + palette_map.insert(VoxelType::Outline, palette.resolve(&VoxelType::Outline)); + palette_map.insert(VoxelType::Shadow, palette.resolve(&VoxelType::Shadow)); Self { palette_map } } diff --git a/crates/exporters/src/mcfunction.rs b/crates/exporters/src/mcfunction.rs index 5245ff8..b31d1bb 100644 --- a/crates/exporters/src/mcfunction.rs +++ b/crates/exporters/src/mcfunction.rs @@ -1,19 +1,18 @@ use std::collections::HashMap; -use lib::{StructureExporter, VoxelType}; +use lib::{BlockPalette, StructureExporter, VoxelType}; pub struct McFunctionExporter { palette: HashMap, } impl McFunctionExporter { - pub fn new(body_block: &str, outline_block: &str) -> Self { - let mut palette = HashMap::new(); - - palette.insert(VoxelType::Body, body_block.to_string()); - palette.insert(VoxelType::Outline, outline_block.to_string()); - - Self { palette } + pub fn new(palette: &BlockPalette) -> Self { + let mut map = HashMap::new(); + map.insert(VoxelType::Body, palette.resolve(&VoxelType::Body)); + map.insert(VoxelType::Outline, palette.resolve(&VoxelType::Outline)); + map.insert(VoxelType::Shadow, palette.resolve(&VoxelType::Shadow)); + Self { palette: map } } } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index e9c0966..0dffd7b 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -12,6 +12,7 @@ pub use font::FontProvider; pub use fonts::ttf_font::TtfFont; pub use grid::VoxelGrid; pub use models::VoxelType; +pub use palette::BlockPalette; pub trait StructureExporter { fn export(&self, grid: &VoxelGrid) -> anyhow::Result>; diff --git a/palettes/default.json b/palettes/default.json new file mode 100644 index 0000000..783465d --- /dev/null +++ b/palettes/default.json @@ -0,0 +1,9 @@ +{ + "name": "Default", + "author": null, + "blocks": { + "body": "minecraft:quartz_block", + "outline": "minecraft:obsidian", + "shadow": null + } +} diff --git a/palettes/gold.json b/palettes/gold.json new file mode 100644 index 0000000..a95fd7f --- /dev/null +++ b/palettes/gold.json @@ -0,0 +1,9 @@ +{ + "name": "Gold", + "author": null, + "blocks": { + "body": "minecraft:gold_block", + "outline": "minecraft:deepslate", + "shadow": null + } +} diff --git a/palettes/ocean.json b/palettes/ocean.json new file mode 100644 index 0000000..500efb1 --- /dev/null +++ b/palettes/ocean.json @@ -0,0 +1,9 @@ +{ + "name": "Ocean", + "author": null, + "blocks": { + "body": "minecraft:blue_concrete", + "outline": "minecraft:white_concrete", + "shadow": "minecraft:gray_concrete" + } +} diff --git a/palettes/stone.json b/palettes/stone.json new file mode 100644 index 0000000..62e28db --- /dev/null +++ b/palettes/stone.json @@ -0,0 +1,9 @@ +{ + "name": "Stone", + "author": null, + "blocks": { + "body": "minecraft:stone", + "outline": "minecraft:cobblestone", + "shadow": null + } +}