use crossterm::cursor::MoveTo; use crossterm::event::{poll, read, Event, KeyCode}; use crossterm::style::{Color, Print, SetForegroundColor}; use crossterm::terminal::{enable_raw_mode, Clear, ClearType}; use crossterm::{QueueableCommand}; use ndarray::{arr1, arr2, Array1}; use std::io::{stdout, Write}; use std::process::exit; use std::thread; use std::time::Duration; const X_DRAW_SCALING_FACTOR: f32 = 2.8; enum Dimension { X, Y, Z, } struct Mesh { points: Vec>, edges: Vec<[usize; 2]>, line_color: Option, node_color: Option, } #[inline] #[rustfmt::skip] fn rotation_matrix(angle: f32, dimension: Dimension) -> ndarray::Array2 { let sin = angle.to_radians().sin(); let cos = angle.to_radians().cos(); match dimension { Dimension::X => arr2(&[ [1., 0., 0.], [0., cos, -sin], [0., sin, cos] ]), Dimension::Y => arr2(&[ [cos, 0., -sin], [0., 1., 0.], [sin, 0., cos] ]), Dimension::Z => arr2(&[ [cos, -sin, 0.], [sin, cos, 0.], [0., 0., 1.] ]) } } #[inline] #[rustfmt::skip] fn scale(x: f32, y: f32, z: f32) -> ndarray::Array2 { arr2(&[ [x, 0., 0.], [0., y, 0.], [0., 0., z], ]) } #[inline] #[rustfmt::skip] fn ortho_matrix() -> ndarray::Array2 { arr2(&[ [1., 0., 0.], [0., 1., 0.], ]) } fn plot_line( origin: &[i32; 2], destination: &[i32; 2], out: &mut impl Write, ) -> anyhow::Result<()> { assert!(origin[0] > 0 && destination[0] > 0); let (origin, destination) = if origin[0] < destination[0] { (origin, destination) } else { (destination, origin) }; let (xdiff, ydiff) = ( destination[0].abs_diff(origin[0]), destination[1].abs_diff(origin[1]), ); if ydiff > xdiff { plot_line_vertical(origin, destination, out) } else { plot_line_horizontal(origin, destination, out) } } /// # CONTRACT /// * origin is *left of* destination (`origin[0] <= destination[0]`) /// * inclination from origin to destination fulfills `-1 <= incl <= 1` /// * line is inside (u16::MAX x u16::MAX) space fn plot_line_horizontal( origin: &[i32; 2], destination: &[i32; 2], out: &mut impl Write, ) -> anyhow::Result<()> { // assert!(destination[0] > origin[0]); let incl = (destination[1] - origin[1]) as f32 / (destination[0] - origin[0]) as f32; // assert!(((-1.)..=1.).contains(&incl)); for i in 0..(destination[0] - origin[0]) { // CONTRACT: i is in u16 space let x = (i + origin[0]) as u16; // CONTRACT: line points are in u16 space let y = ((incl * i as f32).round() + origin[1] as f32) as u16; out.queue(MoveTo(x, y))? .queue(match (incl < -0.4, incl < 0.4) { (false, true) => Print("-"), (false, false) => Print("\\"), (true, true) => Print("/"), _ => unreachable!(), })?; } Ok(()) } /// # CONTRACT /// * origin is *left of* destination (`origin[0] <= destination[0]`) /// * inclination from origin to destination fulfills `incl <= -1` or `incl >= 1` /// * line is inside (u16::MAX x u16::MAX) space fn plot_line_vertical( origin: &[i32; 2], destination: &[i32; 2], out: &mut impl Write, ) -> anyhow::Result<()> { let (origin, destination) = if origin[1] < destination[1] { (origin, destination) } else { (destination, origin) }; let incl = (destination[0] - origin[0]) as f32 / (destination[1] - origin[1]) as f32; // assert!((..=(-1.)).contains(&incl) || (1.0..).contains(&incl)); for i in 0..destination[1] - origin[1] { // CONTRACT: i is in u16 space let y = (i + origin[1]) as u16; // CONTRACT: line points are in u16 space let x = ((incl * i as f32).round() + origin[0] as f32) as u16; out.queue(MoveTo(x, y))? .queue(match (incl > -0.5 && incl < 0.5, incl > 0.) { (true, _) => Print("|"), (false, false) => Print("/"), (false, true) => Print("\\"), })?; } Ok(()) } fn draw_rotating_mesh(meshes: Vec) -> anyhow::Result<()> { let mut angle = (0., 0., 0.); let mut zoom = 1.; loop { if poll(Duration::from_millis(10))? { if let Event::Key(key) = read()? { match key.code { KeyCode::Char('w') => angle.0 += 6., KeyCode::Char('s') => angle.0 -= 6., KeyCode::Char('a') => angle.2 += 6., KeyCode::Char('d') => angle.2 -= 6., KeyCode::Char('q') => angle.1 += 6., KeyCode::Char('e') => angle.1 -= 6., KeyCode::Char(',') => zoom = 0.2f32.max(zoom - 0.02), KeyCode::Char('.') => zoom = 1f32.min(zoom + 0.02), KeyCode::Esc | KeyCode::Backspace | KeyCode::Char('c') => exit(0), _ => {} } } } else { angle.1 += 0.1; } angle.1 %= 360.; // rotation and matmul let x_matrix = rotation_matrix(angle.0, Dimension::X); let y_matrix = rotation_matrix(angle.1, Dimension::Y); let z_matrix = rotation_matrix(angle.2, Dimension::Z); let zoom_matrix = scale(zoom, zoom, zoom); let cam_matrix = ortho_matrix(); let mut stdout = stdout(); stdout.queue(Clear(ClearType::All))?; for mesh in &meshes { let points = &mesh.points; let edges = &mesh.edges; let projected_points = points .iter() .map(|pt| scale(1.6, 1.6, 1.6).dot(pt)) .map(|pt| x_matrix.dot(&pt)) .map(|pt| y_matrix.dot(&pt)) .map(|pt| z_matrix.dot(&pt)) .map(|pt| zoom_matrix.dot(&pt)) .map(|pt| cam_matrix.dot(&pt)) .map(|pt| pt + arr1(&[30., 30.])) // draw shift .collect::>>(); // set edge color before edge drawing stdout.queue(SetForegroundColor(mesh.line_color.unwrap_or(Color::White)))?; for edge in edges.iter() { let origin = &projected_points[edge[0]]; let dest = &projected_points[edge[1]]; plot_line( &[ (origin[0] * X_DRAW_SCALING_FACTOR).round() as i32, origin[1] as i32, ], &[(dest[0] * X_DRAW_SCALING_FACTOR) as i32, dest[1] as i32], &mut stdout, )?; } // set mesh point color stdout.queue(SetForegroundColor(mesh.node_color.unwrap_or(Color::White)))?; for pt in &projected_points { let pt = pt; stdout .queue(MoveTo((pt[0] * X_DRAW_SCALING_FACTOR) as u16, pt[1] as u16))? .queue(Print("■"))?; } } stdout.flush()?; thread::sleep(Duration::from_millis(10)); } unreachable!() } fn main() -> anyhow::Result<()> { enable_raw_mode()?; let random_rectangle = Mesh { points: vec![ arr1(&[-10., 0., 0.]), arr1(&[0., 10., 0.]), arr1(&[10., 0., 0.]), arr1(&[0., -10., 0.]), ], edges: vec![[0, 1], [1, 2], [2, 3], [3, 0]], line_color: Some(Color::Red), node_color: Some(Color::DarkRed), }; let cube = vec![ arr1(&[-10., -10., 10.]), arr1(&[-10., 10., 10.]), arr1(&[-10., -10., -10.]), arr1(&[-10., 10., -10.]), arr1(&[10., -10., 10.]), arr1(&[10., 10., 10.]), arr1(&[10., -10., -10.]), arr1(&[10., 10., -10.]), arr1(&[-6., 0., 0.]), arr1(&[6., 0., 0.]), ]; let edges = vec![ // left face [0usize, 1], [0, 2], [1, 3], [2, 3], // right face [4, 5], [4, 6], [5, 7], [6, 7], // center edges [0, 8], [3, 8], [4, 9], [7, 9], ]; let thing = Mesh { points: vec![ arr1(&[0., 0., 6.]), arr1(&[0., 0., -6.]), arr1(&[0., 6., 0.]), arr1(&[0., -6., 0.]), arr1(&[6., 0., 0.]), arr1(&[-6., 0., 0.]), ], edges: vec![ [0, 2], [0, 3], [0, 4], [0, 5], [1, 2], [1, 3], [1, 4], [1, 5], [3, 4], [3, 5], [2, 4], [2, 5], ], line_color: Some(Color::Blue), node_color: Some(Color::White), }; draw_rotating_mesh(vec![ random_rectangle, thing, Mesh { points: cube, edges, line_color: Some(Color::Green), node_color: Some(Color::White), }, ])?; unreachable!() }