diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 70c372b..e67766c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,7 +4,6 @@ on: push: branches: [ "master" ] pull_request: - env: CARGO_TERM_COLOR: always diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb951aa --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Kodama + +Kodama is a language for describing 3D models. +There is no spec, or CLI as of now. As requirements are evaluated, the spec will be refined to fit users' desires. + +## Mission Statement + +Due to the large complexity of 3D modelling tools such as Blender, it is difficult for beginners to enter the space. +It has been observed in fields such as programming that there is a clear progression curve in terms of the usability of tools. + +For example in programming languages, we do not encourage beginners to learn rust or haskell. +We suggest languages such as Python, JavaScript, or C. +This is because these languages provide a simple interface for writing programs, with the option of using more complicated features of the language. + +This is contrasted with languages such as rust, or haskell, where the minimum level of knowledge for writing programs is much higher. + +This is often called the skill floor. + +It is not common for applications to have a high skill floor. This is often because they use a GUI for all operations. +GUI applications need to have all options visible, or accessible in a short amount of clicks, or keyboard shortcuts. + +As applications scale in complexity, their GUIs have to reflect that. + +It is often necessary to allow users to create interesting things. + +This is in contrast to programming languages, which can be seen as a subset of text based applications. +All the UI of a programming language is captured in a set of UTF-8 encoded keywords, and grammar. + +This then means that no bespoke tools are mandatory for writing programs. +This README was written in vim, but it could easily have been written in vscode, notepad, or even by hovering a magnet over my laptop. + +Kodama aims to become a tool for users to describe 3D models in text. +This allows beginners to only be exposed to the parts of the language they need for their specific case, with any extra features only a google or docs search away. diff --git a/annoying.sh b/annoying.sh new file mode 100644 index 0000000..42c0741 --- /dev/null +++ b/annoying.sh @@ -0,0 +1,9 @@ +cargo clippy -- \ + -Wclippy::all \ + -Wclippy::restriction \ + -Wclippy::correctness \ + -Wclippy::pedantic \ + -Wclippy::nursery \ + -Wclippy::cargo \ + -Wclippy::suspicious \ + -Wclippy::perf diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ee75309 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,211 @@ +use std::f32::consts::PI; + +#[derive(Clone, Copy, Debug)] +struct Point { + x: f32, + y: f32, + z: f32, +} + +impl Point { + const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + pub fn to_obj_string(self) -> String { + format!("v {0} {1} {2}", self.x, self.y, self.z) + } +} + +#[derive(Clone)] +struct Face { + points: Vec, + indexes: Vec, +} + +impl Face { + pub fn new(points: Vec, indexes: Vec) -> Self { + Self { points, indexes } + } + pub fn to_obj_string(self) -> String { + let mut result = String::from("f "); + + result.push_str( + &self + .indexes + .clone() + .into_iter() + .map(|i: u32| -> String { + format!("-{0}", (self.points.len() as u64 - u64::from(i))) + }) + .collect::>() + .join(" "), + ); + result + } +} + +fn vertex_string(points: Vec) -> String { + points + .into_iter() + .map(Point::to_obj_string) + .collect::>() + .join("\n") +} + + +fn render_obj(points: Vec, faces: Vec) -> String { + format!( + r#"{0} +{1} +"#, + vertex_string(points.to_vec()), + faces + .into_iter() + .map(Face::to_obj_string) + .collect::>() + .join("\n") + ) +} + +fn sphere(origin: Point, radius: f32, _detail: u32) -> Result { + // top and bottom point + // create vertical "strips" down the sphere, + // rotating around the Z axis + // Create faces procedurally + + let points = [ + //top + Point::new(origin.x, origin.y + (radius * (2.0 * PI).sin()), origin.z), + //bottom + Point::new(origin.x, origin.y + (radius * (-2.0 * PI).sin()), origin.z), + //north + Point::new(origin.x, origin.y, origin.z + (radius * (2.0 * PI).sin())), + //south + Point::new(origin.x, origin.y, origin.z + (radius * (-2.0 * PI).sin())), + //east + Point::new(origin.x + (radius * (0.0_f32).cos()), origin.y, origin.z), + //west + Point::new(origin.x + (radius * (PI).cos()), origin.y, origin.z), + ] + .to_vec(); + + let faces = [ + // top, north, east + Face::new(points.clone(), vec![0, 2, 4]), + // top, north, west + Face::new(points.clone(), vec![0, 2, 5]), + // top, south, east + Face::new(points.clone(), vec![0, 3, 4]), + // top, south, west + Face::new(points.clone(), vec![0, 3, 5]), + // bottom, north, east + Face::new(points.clone(), vec![1, 2, 4]), + // bottom, north, west + Face::new(points.clone(), vec![1, 2, 5]), + // bottom, south, east + Face::new(points.clone(), vec![1, 3, 4]), + // bottom, south, west + Face::new(points.clone(), vec![1, 3, 5]), + ]; + + Ok(render_obj(points, faces.to_vec())) +} + +fn cone(origin: Point, _detail: i32, _radius: f32) -> String { + let points = [ + Point::new(origin.x, origin.y + (PI / 3.0).sin(), origin.z), + Point::new(origin.x, origin.y, origin.z + 0.0_f32.cos()), + Point::new( + origin.x + (4.0 * PI / 3.0).sin(), + origin.y, + (2.0 * -PI / 3.0).cos(), + ), + Point::new( + origin.x + (2.0 * PI / 3.0).sin(), + origin.y, + (2.0 * -PI / 3.0).cos(), + ), + ]; + + let faces = [ + Face::new(points.to_vec(), vec![1, 2, 3]), + Face::new(points.to_vec(), vec![0, 2, 3]), + Face::new(points.to_vec(), vec![0, 1, 3]), + Face::new(points.to_vec(), vec![0, 2, 1]), + ]; + + render_obj(points.to_vec(), faces.to_vec()) +} + +fn cuboid(origin: Point, sx: f32, sy: f32, sz: f32) -> Result { + println!("GENERATING CUBOID"); + if sx <= 0.0 || sy <= 0.0 || sz <= 0.0 { + return Err( + String::from("could not generate cuboid, side length less than or equal to zero"), + ); + } + let points = [ + Point::new(origin.x, origin.y + sy, origin.z + sz), + Point::new(origin.x, origin.y, origin.z + sz), + Point::new(origin.x + sx, origin.y, origin.z + sz), + Point::new(origin.x + sx, origin.y + sy, origin.z + sz), + Point::new(origin.x, origin.y + sy, origin.z), + Point::new(origin.x, origin.y, origin.z), + Point::new(origin.x + sx, origin.y, origin.z), + Point::new(origin.x + sx, origin.y + sy, origin.z), + ]; + + let faces = [ + Face::new(points.to_vec(), vec![0, 1, 2, 3]), + Face::new(points.to_vec(), vec![7, 6, 5, 4]), + Face::new(points.to_vec(), vec![4, 5, 1, 0]), + Face::new(points.to_vec(), vec![3, 7, 4, 0]), + Face::new(points.to_vec(), vec![3, 2, 6, 7]), + Face::new(points.to_vec(), vec![6, 2, 1, 5]), + ]; + + Ok(render_obj(points.to_vec(), faces.to_vec())) +} + +fn cube(origin: Point, size: f32) -> Result { + println!("GENERATING CUBE"); + if size <= 0.0 { + return Err("ERROR: cannot generate cube of size less than zero".to_string()); + } + cuboid(origin, size, size, size) +} + +pub fn compile(data: &str) -> String { + let mut result = String::new(); + + let lines = data.split('\n'); + for line in lines { + let tokens: Vec<&str> = line.split(' ').collect(); + match *tokens.first().unwrap() { + "cube" => result.push_str( + &cube( + Point::new(0.0, 0.0, 0.0), + tokens[1].parse::().expect("invalid value given"), + ) + .unwrap(), + ), + "cuboid" => result.push_str( + &cuboid( + Point::new(0.0, 0.0, 0.0), + tokens[1].parse::().expect("non numeric value given"), + tokens[2].parse::().expect("non numeric value given"), + tokens[3].parse::().expect("non numeric value given"), + ) + .unwrap(), + ), + "cone" => result.push_str(&cone( + Point::new(0.0, 0.0, 0.0), + 0, + tokens[1].parse::().expect("non numeric value given"), + )), + "sphere" => result.push_str(&sphere(Point::new(0.0, 0.0, 0.0), 10.0, 10).unwrap()), + &_ => println!("{} not supported", tokens[0]), + } + } + result +} diff --git a/src/main.rs b/src/main.rs index 3754d40..9761898 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,138 +1,5 @@ -use std::f32::consts::PI; - -#[derive(Clone)] -struct Point { - x: f32, - y: f32, - z: f32, -} - -impl Point { - pub fn new(x: f32, y: f32, z: f32) -> Self { - Self { x, y, z } - } -} +use kodama::compile; fn main() { - println!("{}", compile("prism 1".to_string()).unwrap()) -} - -fn vertex_string(points: Vec) -> String { - points - .into_iter() - .map(|p| format!("v {0} {1} {2}", p.x, p.y, p.z)) - .collect::>() - .join("\n") -} - -fn prism(_radius: f32) -> Result { - let points = [ - Point::new(0.000000, (PI / 3.0).sin(), 0.000000), - Point::new(0.0, 0.000000, 0.0_f32.cos()), - Point::new((4.0 * PI / 3.0).sin(), 0.000000, (2.0 * -PI / 3.0).cos()), - Point::new((2.0 * PI / 3.0).sin(), 0.000000, (2.0 * -PI / 3.0).cos()), - ]; - - Ok(format!( - r#"{0} -f -3 -2 -1 -f -4 -2 -1 -f -4 -3 -1 -f -4 -2 -3 -"#, - vertex_string(points.to_vec()) - )) -} - -fn cuboid(sx: f32, sy: f32, sz: f32) -> Result { - println!("GENERATING CUBOID"); - if sx <= 0.0 || sy <= 0.0 || sz <= 0.0 { - return Err( - "could not generate cuboid, side length less than or equal to zero".to_string(), - ); - } - let points = [ - Point::new(0.0, sy, sz), - Point::new(0.0, 0.0, sz), - Point::new(sx, 0.0, sz), - Point::new(sx, sy, sz), - Point::new(0.0, sy, 0.0), - Point::new(0.0, 0.0, 0.0), - Point::new(sx, 0.0, 0.0), - Point::new(sx, sy, 0.0), - ]; - Ok(format!( - r#"{0} -f -8 -7 -6 -5 -f -1 -2 -3 -4 -f -5 -6 -2 -1 -f -4 -8 -5 -1 -f -4 -3 -7 -8 -f -7 -3 -2 -6 -"#, - vertex_string(points.to_vec()) - )) -} - -fn cube(size: f32) -> Result { - println!("GENERATING CUBE"); - if size <= 0.0 { - return Err("ERROR: cannot generate cube of size less than zero".to_string()); - } - cuboid(size, size, size) -} - -fn compile(data: String) -> Result { - let mut result = String::from(""); - - let lines = data.split('\n'); - for line in lines { - let tokens: Vec<&str> = line.split(' ').collect(); - match tokens[0] { - "cube" => result - .push_str(&cube(tokens[1].parse::().expect("invalid value given")).unwrap()), - "cuboid" => result.push_str( - &cuboid( - tokens[1].parse::().expect("non numeric value given"), - tokens[2].parse::().expect("non numeric value given"), - tokens[3].parse::().expect("non numeric value given"), - ) - .unwrap(), - ), - "prism" => result.push_str( - &prism(tokens[1].parse::().expect("non numeric value given")).unwrap(), - ), - &_ => println!("{} not supported", tokens[0]), - } - } - Ok(result) -} - -#[cfg(test)] -mod tests { - use crate::compile; - use std::fs; - - fn run_test(input_path: String, result_path: String) { - let input = fs::read_to_string(&input_path).expect("could not load file"); - let result = fs::read_to_string(&result_path).expect("could not load file"); - assert_eq!(compile(input), Ok(result)); - } - #[test] - fn walk_tests() { - match fs::read_dir("./tests/") { - Ok(entries) => { - for entry in entries { - let path = entry.expect("could not open dir").path(); - let input = path.join("main.kda"); - let output = path.join("result.obj"); - run_test( - input.into_os_string().into_string().unwrap(), - output.into_os_string().into_string().unwrap(), - ) - } - } - Err(e) => eprintln!("{}", e) - } - } + println!("{}", compile("sphere")); } diff --git a/tests/cube/result.obj b/tests/cube/result.obj index e051cd7..36fad5d 100644 --- a/tests/cube/result.obj +++ b/tests/cube/result.obj @@ -8,7 +8,7 @@ v 2 0 0 v 2 2 0 f -8 -7 -6 -5 f -1 -2 -3 -4 -f -5 -6 -2 -1 -f -4 -8 -5 -1 f -4 -3 -7 -8 -f -7 -3 -2 -6 +f -5 -1 -4 -8 +f -5 -6 -2 -1 +f -2 -6 -7 -3 diff --git a/tests/cuboid/result.obj b/tests/cuboid/result.obj index 8fc732c..bde2408 100644 --- a/tests/cuboid/result.obj +++ b/tests/cuboid/result.obj @@ -8,7 +8,7 @@ v 2 0 0 v 2 3 0 f -8 -7 -6 -5 f -1 -2 -3 -4 -f -5 -6 -2 -1 -f -4 -8 -5 -1 f -4 -3 -7 -8 -f -7 -3 -2 -6 +f -5 -1 -4 -8 +f -5 -6 -2 -1 +f -2 -6 -7 -3 diff --git a/tests/prism/main.kda b/tests/prism/main.kda index a494448..07a309b 100644 --- a/tests/prism/main.kda +++ b/tests/prism/main.kda @@ -1 +1 @@ -prism 1 +cone 1 diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..4add9ae --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,32 @@ +#[cfg(test)] +mod tests { + use std::fs; + use kodama::compile; + + fn run_test(input_path: String, result_path: String) { + let input = fs::read_to_string(&input_path).expect("could not load file"); + let result = fs::read_to_string(&result_path).expect("could not load file"); + assert_eq!(compile(&input), result); + } + #[test] + fn walk_tests() { + let test_dir = "./tests"; + match fs::read_dir(test_dir) { + Ok(entries) => { + for entry in entries { + let path = entry.expect("could not open dir").path(); + if !path.is_dir(){ + continue + } + let input = path.join("main.kda"); + let output = path.join("result.obj"); + run_test( + input.into_os_string().into_string().unwrap(), + output.into_os_string().into_string().unwrap(), + ) + } + } + Err(e) => eprintln!("Error: {}", e), + } + } +}