Enhance CLI functionality with palette support and update exporters to use BlockPalette

This commit is contained in:
2026-03-23 01:21:21 +01:00
parent 1893b3427f
commit 484415870a
12 changed files with 149 additions and 29 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
*.mcfunction

27
Cargo.lock generated
View File

@@ -209,6 +209,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -255,6 +261,8 @@ dependencies = [
"clap", "clap",
"exporters", "exporters",
"lib", "lib",
"serde",
"serde_json",
"thiserror", "thiserror",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -381,6 +389,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -547,3 +568,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -17,6 +17,8 @@ lib = { path = "../lib" }
exporters = { path = "../exporters" } exporters = { path = "../exporters" }
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
anyhow = { workspace = true } anyhow = { workspace = true }
serde = { workspace = true }
serde_json = "1"
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-subscriber = { workspace = true, features = ["env-filter"] }

View File

@@ -2,26 +2,34 @@ use std::{fs, path::PathBuf};
use clap::Parser; use clap::Parser;
use exporters::McFunctionExporter; use exporters::McFunctionExporter;
use lib::{StructureExporter, TextBuilder, TtfFont}; use lib::{BlockPalette, StructureExporter, TextBuilder, TtfFont};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Cli { struct Cli {
/// The text you want to generate /// The text you want to generate
#[arg(short, long)] #[arg(short, long, required_unless_present = "list_palettes")]
text: String, text: Option<String>,
/// Path to the .ttf font file /// Path to the .ttf font file
#[arg(short, long)] #[arg(short, long, required_unless_present = "list_palettes")]
font: PathBuf, font: Option<PathBuf>,
/// How many blocks deep the text should be /// How many blocks deep the text should be
#[arg(short, long, default_value_t = 1)] #[arg(short, long, default_value_t = 1)]
depth: u32, depth: u32,
/// The Minecraft block ID to use for the text body /// Name of a built-in palette preset (from the palettes/ directory)
#[arg(short, long, default_value = "minecraft:quartz_block")] #[arg(short, long, conflicts_with = "palette_file")]
block: String, palette: Option<String>,
/// Path to a JSON palette file
#[arg(long)]
palette_file: Option<PathBuf>,
/// List available palette presets and exit
#[arg(long)]
list_palettes: bool,
/// Output file path (without extension) /// Output file path (without extension)
#[arg(short, long, default_value = "output")] #[arg(short, long, default_value = "output")]
@@ -32,26 +40,70 @@ struct Cli {
size: f32, size: f32,
} }
fn palettes_dir() -> PathBuf {
std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.join("palettes")
}
fn load_palette_by_name(name: &str) -> anyhow::Result<BlockPalette> {
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<()> { pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
tracing::info!("loading font from: {:?}", cli.font); if cli.list_palettes {
let font_bytes = fs::read(&cli.font)?; 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) let text = cli.text.unwrap();
.map_err(|e| anyhow::anyhow!(e))?; 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 builder = TextBuilder::new(&font).with_depth(cli.depth);
let grid = builder.generate(&cli.text); let grid = builder.generate(&text);
tracing::info!( tracing::info!(
"grid generated: {}x{}x{}", "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 output_bytes = exporter.export(&grid)?;
let mut out_path = cli.out.clone(); let mut out_path = cli.out.clone();

View File

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

View File

@@ -7,7 +7,7 @@ use std::{
use anyhow::Context; use anyhow::Context;
use flate2::Compression; use flate2::Compression;
use flate2::write::GzEncoder; use flate2::write::GzEncoder;
use lib::{StructureExporter, VoxelType}; use lib::{BlockPalette, StructureExporter, VoxelType};
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@@ -61,10 +61,11 @@ pub struct LitematicaExporter {
} }
impl 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(); let mut palette_map = HashMap::new();
palette_map.insert(VoxelType::Body, body_block.to_string()); palette_map.insert(VoxelType::Body, palette.resolve(&VoxelType::Body));
palette_map.insert(VoxelType::Outline, outline_block.to_string()); palette_map.insert(VoxelType::Outline, palette.resolve(&VoxelType::Outline));
palette_map.insert(VoxelType::Shadow, palette.resolve(&VoxelType::Shadow));
Self { palette_map } Self { palette_map }
} }

View File

@@ -1,19 +1,18 @@
use std::collections::HashMap; use std::collections::HashMap;
use lib::{StructureExporter, VoxelType}; use lib::{BlockPalette, StructureExporter, VoxelType};
pub struct McFunctionExporter { pub struct McFunctionExporter {
palette: HashMap<VoxelType, String>, palette: HashMap<VoxelType, String>,
} }
impl McFunctionExporter { impl McFunctionExporter {
pub fn new(body_block: &str, outline_block: &str) -> Self { pub fn new(palette: &BlockPalette) -> Self {
let mut palette = HashMap::new(); let mut map = HashMap::new();
map.insert(VoxelType::Body, palette.resolve(&VoxelType::Body));
palette.insert(VoxelType::Body, body_block.to_string()); map.insert(VoxelType::Outline, palette.resolve(&VoxelType::Outline));
palette.insert(VoxelType::Outline, outline_block.to_string()); map.insert(VoxelType::Shadow, palette.resolve(&VoxelType::Shadow));
Self { palette: map }
Self { palette }
} }
} }

View File

@@ -12,6 +12,7 @@ pub use font::FontProvider;
pub use fonts::ttf_font::TtfFont; pub use fonts::ttf_font::TtfFont;
pub use grid::VoxelGrid; pub use grid::VoxelGrid;
pub use models::VoxelType; pub use models::VoxelType;
pub use palette::BlockPalette;
pub trait StructureExporter { pub trait StructureExporter {
fn export(&self, grid: &VoxelGrid) -> anyhow::Result<Vec<u8>>; fn export(&self, grid: &VoxelGrid) -> anyhow::Result<Vec<u8>>;

9
palettes/default.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "Default",
"author": null,
"blocks": {
"body": "minecraft:quartz_block",
"outline": "minecraft:obsidian",
"shadow": null
}
}

9
palettes/gold.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "Gold",
"author": null,
"blocks": {
"body": "minecraft:gold_block",
"outline": "minecraft:deepslate",
"shadow": null
}
}

9
palettes/ocean.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "Ocean",
"author": null,
"blocks": {
"body": "minecraft:blue_concrete",
"outline": "minecraft:white_concrete",
"shadow": "minecraft:gray_concrete"
}
}

9
palettes/stone.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "Stone",
"author": null,
"blocks": {
"body": "minecraft:stone",
"outline": "minecraft:cobblestone",
"shadow": null
}
}