You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

830 lines
28 KiB

use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg, SubCommand};
use nbt::encode::write_gzip_compound_tag;
use nbt::{CompoundTag, Tag};
use rgb::*;
use std::collections::HashMap;
use std::fs;
use std::io::{Error, ErrorKind, Read, Write};
mod astree;
mod error;
mod ngon;
mod parser;
mod star;
mod triangle;
macro_rules! value_message {
($n:ident, $t:expr) => {
Err(format!("<{}> is not a valid {}", $n, $t))
};
}
macro_rules! csv_line {
($f:ident, $n:expr, $t:expr, $c:expr, $s:expr, $b:expr) => {
writeln!($f, "{},{},{},{},{}", $n, $t, $c, $s, $b)?;
};
}
fn io_error(err: Error, path: &str) -> String {
match err.kind() {
ErrorKind::NotFound => format!("{} not found", path),
ErrorKind::PermissionDenied => format!("Permission to access {} denied", path),
ErrorKind::AlreadyExists => format!("{} already exists", path),
_ => format!("Unexpected error accessing {}", path),
}
}
fn main() -> Result<(), error::Error> {
use error::Error;
let matches = App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.arg(
Arg::with_name("scale")
.short("s")
.long("scale")
.help("The scale parameter for the object")
.takes_value(true)
.multiple(false)
.value_name("S")
.validator(|n: String| -> Result<(), String> {
if let Ok(scale) = n.parse::<f64>() {
if scale >= 0_f64 {
return Ok(());
}
}
value_message!(n,"scale value")
}),
)
.arg(
Arg::with_name("block")
.short("b")
.long("block")
.help("The minecraft block to be used in the Litematica output, defaults to minecraft:stone")
.takes_value(true)
.multiple(false),
)
.arg(
Arg::with_name("offset")
.short("o")
.long("offset")
.help("Offset the computation by half a block")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("xoffset")
.short("x")
.long("xoffset")
.help("Offset the x computation by half a block")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("yoffset")
.short("y")
.long("yoffset")
.help("Offset the y computation by half a block")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("zoffset")
.short("z")
.long("zoffset")
.help("Offset the z computation by half a block")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("debug")
.short("d")
.long("debug")
.help("Show intermediate steps")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("graph")
.short("g")
.long("graph")
.help("Output graph of internal state")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("remake")
.short("r")
.long("remake")
.help("Remake definition file from internal state")
.takes_value(false)
.multiple(false),
)
.arg(
Arg::with_name("test")
.short("t")
.long("test")
.help("Parses the input, does not output")
.takes_value(false)
.multiple(false),
)
.subcommand(SubCommand::with_name("star")
.about("Make a star")
.arg(
Arg::with_name("factor")
.short("f")
.long("factor")
.help("The scale parameter for the outer points of the star")
.takes_value(true)
.multiple(false)
.value_name("F")
.validator(|n: String| -> Result<(), String> {
if let Ok(scale) = n.parse::<f64>() {
if scale >= 0_f64 {
return Ok(());
}
}
value_message!(n,"outer points scale value")
}),
)
.arg(
Arg::with_name("angle")
.short("a")
.long("angle")
.help("Angle in radians by which to rotate the star")
.takes_value(true)
.multiple(false)
.value_name("RAD")
.validator(|n: String| -> Result<(), String> {
if n.parse::<f64>().is_err() {
value_message!(n,"angle in radians")
}else{
Ok(())
}
}),
)
.arg(
Arg::with_name("degrees")
.short("e")
.long("degrees")
.help("Angle in degrees by which to rotate the star")
.conflicts_with("angle")
.takes_value(true)
.multiple(false)
.value_name("DEG")
.validator(|n: String| -> Result<(), String> {
if n.parse::<f64>().is_err() {
value_message!(n,"angle in degrees")
}else{
Ok(())
}
}),
)
.arg(
Arg::with_name("N")
.help("The number of points of the star")
.required(true)
.index(1)
.validator(|n: String| -> Result<(), String> {
if let Ok(sides) = n.parse::<u8>() {
if (3..=100).contains(&sides) {
return Ok(());
}
}
value_message!(n,"number of points")
}),
)
.arg(
Arg::with_name("OUTPUT_DIR")
.help("The folder where the output images will be stored")
.required(true)
.index(2)
.validator(move |path: String| -> Result<(), String> {
match fs::create_dir(&path) {
Ok(_) => Ok(()),
Err(error) => Err(io_error(error, &path)),
}
})
.conflicts_with("test"),
)
)
.subcommand(SubCommand::with_name("ngon")
.about("Make an ngon")
.arg(
Arg::with_name("angle")
.short("a")
.long("angle")
.help("Angle in radians by which to rotate the ngon")
.takes_value(true)
.multiple(false)
.value_name("RAD")
.validator(|n: String| -> Result<(), String> {
if n.parse::<f64>().is_err() {
value_message!(n,"angle in radians")
}else{
Ok(())
}
}),
)
.arg(
Arg::with_name("degrees")
.short("e")
.long("degrees")
.help("Angle in degrees by which to rotate the ngon")
.conflicts_with("angle")
.takes_value(true)
.multiple(false)
.value_name("DEG")
.validator(|n: String| -> Result<(), String> {
if n.parse::<f64>().is_err() {
value_message!(n,"angle in degrees")
}else{
Ok(())
}
}),
)
.arg(
Arg::with_name("N")
.help("The number of sides of the ngon")
.required(true)
.index(1)
.validator(|n: String| -> Result<(), String> {
if let Ok(sides) = n.parse::<u8>() {
if (3..=100).contains(&sides) {
return Ok(());
}
}
value_message!(n,"number of sides")
}),
)
.arg(
Arg::with_name("OUTPUT_DIR")
.help("The folder where the output images will be stored")
.required(true)
.index(2)
.validator(move |path: String| -> Result<(), String> {
match fs::create_dir(&path) {
Ok(_) => Ok(()),
Err(error) => Err(io_error(error, &path)),
}
})
.conflicts_with("test"),
)
)
.subcommand(SubCommand::with_name("solid")
.about("Read mathematical description of solid")
.arg(
Arg::with_name("FILE")
.help("The file describing the shape to map")
.required(true)
.index(1)
.validator(move |path: String| -> Result<(), String> {
match fs::File::open(&path) {
Ok(_) => Ok(()),
Err(error) => Err(io_error(error, &path)),
}
})
)
.arg(
Arg::with_name("OUTPUT_DIR")
.help("The folder where the output images will be stored")
.required(true)
.index(2)
.validator(move |path: String| -> Result<(), String> {
match fs::create_dir(&path) {
Ok(_) => Ok(()),
Err(error) => Err(io_error(error, &path)),
}
})
.conflicts_with("test"),
)
)
.get_matches();
let scale = matches.value_of("scale").map(|s| s.parse::<f64>().unwrap());
let debug = matches.is_present("debug");
let offset = matches.is_present("offset");
let toggle_x = matches.is_present("xoffset");
let toggle_y = matches.is_present("yoffset");
let toggle_z = matches.is_present("zoffset");
let offset_x = offset ^ toggle_x;
let offset_y = offset ^ toggle_y;
let offset_z = offset ^ toggle_z;
if debug {
println!(
"Offset toggles:\n\tGlobal: {}\n\tx toggle: {}\n\ty toggle: {}\n\tz toggle: {}",
offset, toggle_x, toggle_y, toggle_z
);
println!(
"Offsets:\n\tx offset: {}\n\ty offset: {}\n\tz offset: {}",
offset_x, offset_y, offset_z
);
}
let graph = matches.is_present("graph");
let remake = matches.is_present("remake");
let test = matches.is_present("test");
let mut output_folder = ".";
let structure: astree::Structure;
if let Some(submatches) = matches.subcommand_matches("solid") {
let mut data = String::new();
let mut object_description = fs::File::open(submatches.value_of("FILE").unwrap()).unwrap();
output_folder = submatches.value_of("OUTPUT_DIR").unwrap_or(output_folder);
let read_count = object_description.read_to_string(&mut data)?;
if debug {
println!(
"\nRead {} bytes, scale is {}",
read_count,
scale.unwrap_or(1.0_f64)
);
}
structure = parser::parse(
&data,
submatches.value_of("FILE").unwrap(),
submatches.value_of("OUTPUT_DIR"),
debug,
)?;
} else if let Some(submatches) = matches.subcommand_matches("ngon") {
let mut labels = HashMap::new();
let angleident;
if let Some(value) = submatches.value_of("angle") {
let angle_exp = astree::Expression::float(value.parse::<f64>().unwrap());
labels.insert(String::from("@clangle"), angle_exp);
angleident = Some(String::from("@clangle"));
} else if let Some(value) = submatches.value_of("degrees") {
let angle_exp = astree::Expression::float(value.parse::<f64>().unwrap().to_radians());
labels.insert(String::from("@clangle"), angle_exp);
angleident = Some(String::from("@clangle"));
} else {
angleident = None;
}
output_folder = submatches.value_of("OUTPUT_DIR").unwrap_or(output_folder);
structure = ngon::generate::<String, f64>(
submatches
.value_of("N")
.map(|n| n.parse::<u8>().unwrap())
.unwrap(),
None,
(None, None),
angleident,
None,
Some(labels),
)?;
} else if let Some(submatches) = matches.subcommand_matches("star") {
let mut labels = HashMap::new();
let angleident;
if let Some(value) = submatches.value_of("angle") {
let angle_exp = astree::Expression::float(value.parse::<f64>().unwrap());
labels.insert(String::from("@clangle"), angle_exp);
angleident = Some(String::from("@clangle"));
} else if let Some(value) = submatches.value_of("degrees") {
let angle_exp = astree::Expression::float(value.parse::<f64>().unwrap().to_radians());
labels.insert(String::from("@clangle"), angle_exp);
angleident = Some(String::from("@clangle"));
} else {
angleident = None;
}
let star_factor = submatches
.value_of("factor")
.map(|f| f.parse::<f64>().unwrap());
output_folder = submatches.value_of("OUTPUT_DIR").unwrap_or(output_folder);
structure = star::generate::<String, f64>(
submatches
.value_of("N")
.map(|n| n.parse::<u8>().unwrap())
.unwrap(),
None,
(None, None),
angleident,
None,
star_factor,
Some(labels),
)?;
} else {
println!("{}", matches.usage());
return Err(Error::MissingSubCommand);
}
let (assigns, limits, tree) = structure;
let idents = assigns.unwrap_or_default();
let ident_arg = Some(&idents);
let mut min_x: Option<i64> = None;
let mut max_x: Option<i64> = None;
let mut min_y: Option<i64> = None;
let mut max_y: Option<i64> = None;
let mut min_z: Option<i64> = None;
let mut max_z: Option<i64> = None;
let mut vars = HashMap::new();
vars.insert('s', scale.unwrap_or(1_f64));
for limit in &limits {
for dep in limit.min.var_dependencies(&ident_arg)? {
if dep != 's' {
eprintln!("Boundaries can only refer to s, not {}", dep);
return Err(Error::IllegalVarInBoundary);
}
}
for dep in limit.max.var_dependencies(&ident_arg)? {
if dep != 's' {
eprintln!("Boundaries can only refer to s, not {}", dep);
return Err(Error::IllegalVarInBoundary);
}
}
let var_arg = Some(&vars);
let min =
(limit.min.eval(&ident_arg, &var_arg)?.floor() as i64) - if offset { 1 } else { 0 };
let max = limit.max.eval(&ident_arg, &var_arg)?.ceil() as i64;
match limit.var {
'x' => {
min_x = Some(min);
max_x = Some(max);
}
'y' => {
min_y = Some(min);
max_y = Some(max);
}
'z' => {
min_z = Some(min);
max_z = Some(max);
}
c => {
eprintln!("Bounded variables are x,y,z only, not {}", c);
return Err(Error::IllegarBoundedVar);
}
}
}
let mut unbounded = false;
if min_x.is_none() || max_x.is_none() {
unbounded = true;
eprintln!("x is unbounded");
}
if min_y.is_none() || max_y.is_none() {
unbounded = true;
eprintln!("y is unbounded");
}
if min_z.is_none() || max_z.is_none() {
unbounded = true;
eprintln!("z is unbounded");
}
if unbounded {
return Err(Error::UnboundedVar);
}
if debug {
println!("\n{:?}", tree);
}
// Print graph
if graph {
let mut gv_file = fs::File::create(format! {"{}/state.gv",output_folder})?;
writeln!(
gv_file,
"/* Graph file generated from {} v{}, by {} */\n\n",
crate_name!(),
crate_version!(),
crate_authors!()
)?;
writeln!(gv_file, "digraph State {{")?;
// Print main condition
let tree_nodes = tree.graph(&mut gv_file, 1)?;
// Print ident definitions
let mut max_node = tree_nodes;
for (label, expression) in &idents {
writeln!(
gv_file,
"\tnode{} [label=\"{}\",shape=cds];",
max_node + 1,
label
)?;
writeln!(gv_file, "\tnode{} -> node{};", max_node + 1, max_node + 2)?;
max_node = expression.graph(&mut gv_file, max_node + 2)?;
}
writeln!(gv_file, "}}")?;
}
// Print remake
if remake {
let mut remake_file = fs::File::create(format! {"{}/remake.solid",output_folder})?;
writeln!(
remake_file,
"# Solid specifications remade by {} v{}, by {} */\n\n",
crate_name!(),
crate_version!(),
crate_authors!()
)?;
let lookup = Some(&idents);
// Print tree and boundary dependencies
let mut complete_deps = tree.ident_dependencies(&lookup)?;
for limit in &limits {
let limit_deps = limit.ident_dependencies(&lookup)?;
for dep in limit_deps {
complete_deps.insert(dep);
}
}
let mut vector_deps: Vec<(String, usize)> = complete_deps.into_iter().collect();
vector_deps.sort_unstable_by(|(_, i), (_, j)| i.cmp(j));
for dep in vector_deps {
let (name, _) = dep;
let definition = idents
.get(&name)
.ok_or::<Error>(Error::MissingIdentAssignment)?;
writeln!(remake_file, "define {} {}", name, definition)?;
}
writeln!(remake_file, "\n")?;
// Print limits
for limit in &limits {
writeln!(remake_file, "{}", limit)?;
}
writeln!(remake_file, "\n")?;
// Print tree
writeln!(remake_file, "{}", tree)?;
}
if test {
return Ok(());
}
let min_x: i64 = min_x.unwrap();
let max_x: i64 = max_x.unwrap();
let min_y: i64 = min_y.unwrap();
let max_y: i64 = max_y.unwrap();
let min_z: i64 = min_z.unwrap();
let max_z: i64 = max_z.unwrap();
let width: i64 = 1 + max_x - min_x;
let height: i64 = 1 + max_y - min_y;
let depth: i64 = 1 + max_z - min_z;
let pix_width: usize = 6 * (width as usize) + 1;
let pix_height: usize = 6 * (height as usize) + 1;
let pix_size: usize = pix_width * pix_height;
let lite_size: usize = (((width as usize) * (height as usize) * (depth as usize)) / 32) + 1;
let mut lite_block_data: Vec<i64> = Vec::with_capacity(lite_size);
let mut working: i64 = 0;
let mut counter: u8 = 0;
let mut total_blocks: i32 = 0;
let mut total_volume: i32 = 0;
let filled_in = RGBA8::new(0, 0, 0, 255); // Black
let empty = RGBA8::new(255, 255, 255, 255); // White
let multiple_filled_in = RGBA8::new(0, 0, 255, 255); // Blue
let multiple_empty = RGBA8::new(255, 255, 0, 255); // Yellow
let grid = RGBA8::new(255, 128, 128, 255); // Coral (Grid)
let transparent = RGBA8::new(255, 255, 255, 0); // Transparent White
let top_size: usize = (width as usize) * (height as usize);
let mut top_view: Vec<RGBA8> = Vec::with_capacity(top_size);
top_view.resize(top_size, transparent);
for z in min_z..=max_z {
let name = format! {"{}/layer{:04}.png",output_folder,1 + z - min_z};
let mut pixels: Vec<RGBA8> = Vec::with_capacity(pix_size);
pixels.resize(pix_size, grid);
let current_depth = z - min_z;
let ratio = (current_depth * 192 / depth) as u8;
let top_color = RGBA8::new(ratio, ratio, ratio, 255);
for y in min_y..=max_y {
let square_start_y = 6 * (y - min_y) as usize;
let grid_y = y.abs() % 10 == 0;
for x in min_x..=max_x {
let x_f: f64 = (x as f64) + if offset_x { 0.5_f64 } else { 0_f64 };
let y_f: f64 = (y as f64) + if offset_y { 0.5_f64 } else { 0_f64 };
let z_f: f64 = (z as f64) + if offset_z { 0.5_f64 } else { 0_f64 };
let rho: f64 = (x_f.powi(2) + y_f.powi(2)).sqrt();
let r: f64 = (z_f.powi(2) + rho.powi(2)).sqrt();
let tht: f64 = (z_f / rho).atan();
let phi: f64;
if rho < 2_f64 * f64::EPSILON {
phi = f64::NAN;
} else if y_f >= 0_f64 {
phi = (x_f / rho).acos();
} else {
phi = -((x_f / rho).acos());
}
vars.insert('x', x_f);
vars.insert('y', y_f);
vars.insert('z', z_f);
vars.insert('ρ', rho);
vars.insert('φ', phi);
vars.insert('r', r);
vars.insert('θ', tht);
let var_arg = Some(&vars);
let square_start_x = 6 * (x - min_x) as usize;
let grid = if grid_y { true } else { x.abs() % 10 == 0 };
let is_filled = tree.eval(&ident_arg, &var_arg)?;
let color = match (is_filled, grid) {
(false, false) => empty,
(false, true) => multiple_empty,
(true, false) => filled_in,
(true, true) => multiple_filled_in,
};
if is_filled {
let offset: usize = ((y - min_y) * width + (x - min_x)) as usize;
top_view[offset] = top_color;
}
for pix_x in 0..5 {
for pix_y in 0..5 {
let offset =
(1 + square_start_y + pix_y) * pix_width + 1 + square_start_x + pix_x;
pixels[offset] = color;
}
}
let new_block: i64 = if is_filled { 1 } else { 0 } << (counter * 2);
working |= new_block;
counter += 1;
if counter >= 32 {
lite_block_data.push(working);
working = 0;
counter = 0;
}
total_volume += 1;
total_blocks += if is_filled { 1 } else { 0 };
}
}
lodepng::encode32_file(name, &pixels, pix_width, pix_height)?;
}
if counter != 0 {
lite_block_data.push(working);
}
// Top View image
lodepng::encode32_file(
format! {"{}/top_view.png",output_folder},
&top_view,
width as usize,
height as usize,
)?;
// Manifest
let mut manif_file = fs::File::create(format! {"{}/manifest.csv",output_folder})?;
writeln!(manif_file, "Unit,Total,Combined,Stacks,Blocks")?;
writeln!(manif_file, ",,,,")?;
let stacks = if total_blocks % 64 == 0 {
total_blocks / 64
} else {
1 + total_blocks / 64
};
let chests = if stacks % 27 == 0 {
stacks / 27
} else {
1 + stacks / 27
};
let doubles = if chests % 2 == 0 {
chests / 2
} else {
1 + chests / 2
};
let combined_doubles = total_blocks / (64 * 54);
let combined_chests = (total_blocks % (64 * 54)) / (27 * 64);
let combined_stacks = (total_blocks % (64 * 27)) / 64;
let combined_blocks = total_blocks % 64;
csv_line!(
manif_file,
"Double Chests",
doubles,
combined_doubles,
54,
3456
);
csv_line!(manif_file, "Chests", chests, combined_chests, 27, 1728);
csv_line!(manif_file, "Stacks", stacks, combined_stacks, 1, 64);
csv_line!(manif_file, "Blocks", total_blocks, combined_blocks, '-', 1);
writeln!(manif_file, ",,,,")?;
let gold_picks = if total_blocks % 32 == 0 {
total_blocks / 32
} else {
1 + total_blocks / 32
};
let wood_picks = if total_blocks % 59 == 0 {
total_blocks / 59
} else {
1 + total_blocks / 59
};
let stone_picks = if total_blocks % 131 == 0 {
total_blocks / 131
} else {
1 + total_blocks / 131
};
let iron_picks = if total_blocks % 250 == 0 {
total_blocks / 250
} else {
1 + total_blocks / 250
};
let diamond_picks = if total_blocks % 1561 == 0 {
total_blocks / 1561
} else {
1 + total_blocks / 1561
};
let netherite_picks = if total_blocks % 2031 == 0 {
total_blocks / 2031
} else {
1 + total_blocks / 2031
};
csv_line!(manif_file, "Gold Picks", gold_picks, '-', '-', 32);
csv_line!(manif_file, "Wood Picks", wood_picks, '-', '-', 59);
csv_line!(manif_file, "Stone Picks", stone_picks, '-', 2, 131);
csv_line!(manif_file, "Iron Picks", iron_picks, '-', 3, 250);
csv_line!(manif_file, "Diamond Picks", diamond_picks, '-', 24, 1561);
csv_line!(
manif_file,
"Netherite Picks",
netherite_picks,
'-',
31,
2031
);
// Litematica schematic
let mut enclosing_size = CompoundTag::new();
enclosing_size.insert_i32("x", width as i32);
enclosing_size.insert_i32("y", depth as i32);
enclosing_size.insert_i32("z", height as i32);
let region_size = enclosing_size.clone();
let mut metadata = CompoundTag::new();
metadata.insert("EnclosingSize", enclosing_size);
metadata.insert_str("Author", crate_name!());
metadata.insert_str("Description", crate_version!());
metadata.insert_str("Name", output_folder);
metadata.insert_i32("RegionCount", 1);
metadata.insert_i32("TotalBlocks", total_blocks);
metadata.insert_i32("TotalVolume", total_volume);
let mut lite = CompoundTag::new();
lite.insert_i32("Version", 4);
lite.insert_i32("MinecraftDataVersion", 1343);
lite.insert("Metadata", metadata);
let mut region = CompoundTag::new();
region.insert("Size", region_size);
let mut region_position = CompoundTag::new();
region_position.insert_i32("x", 0);
region_position.insert_i32("y", 0);
region_position.insert_i32("z", 0);
region.insert("Position", region_position);
let mut block_state_palette: Vec<Tag> = Vec::with_capacity(2);
let mut air = CompoundTag::new();
air.insert_str("Name", "minecraft:air");
block_state_palette.push(Tag::from(air));
let mut block = CompoundTag::new();
block.insert_str(
"Name",
matches.value_of("block").unwrap_or("minecraft:stone"),
);
block_state_palette.push(Tag::from(block));
region.insert("BlockStatePalette", block_state_palette);
region.insert_i64_vec("BlockStates", lite_block_data);
let mut regions = CompoundTag::new();
regions.insert(output_folder, region);
lite.insert("Regions", regions);
let mut lite_file = fs::File::create(format!("{}/{}.litematic", output_folder, output_folder))?;
write_gzip_compound_tag(&mut lite_file, &lite)?;
Ok(())
}