1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crate::elements::{
    view::{utils, ColChar, Modifier},
    Line, PixelContainer, Point, Polygon, Text, Vec2D,
};
pub mod face;
pub mod transform3d;
pub mod vec3d;
pub use face::Face;
pub use transform3d::Transform3D;
pub use vec3d::Vec3D;

/// `DisplayMode` determines how the [`Viewport`] renders our 3D objects. This is the Gemini equivalent of Blender's Viewport Shading options
/// - [`DisplayMode::Debug`] does the same thing, but shows the vertices as the indices that represent them (this is useful when you are constructing a mesh)
/// - [`DisplayMode::Points`] only renders the object's vertices as single pixels with the [`ColChar`] chosen with the `fill_char` enum parameter
/// - [`DisplayMode::Wireframe`] renders the edges of the meshes, without filling in the shapes. You can choose whether you want to render with backface culling using the `backface_culling` enum parameter
/// - [`DisplayMode::Solid`] renders the full faces of all the meshes. This is normally the final render
pub enum DisplayMode {
    Debug,
    Points { fill_char: ColChar },
    Wireframe { backface_culling: bool },
    Solid,
}

/// The `Viewport` handles printing 3D objects to a 2D [`View`](crate::elements::View), and also acts as the scene's camera.
pub struct Viewport {
    /// How the Viewport is oriented in the 3D scene
    pub transform: Transform3D,
    /// The Viewport's field of view
    pub fov: f64,
    /// The center of the view you intend to print to. You can use `View.center()` as the input for this
    pub origin: Vec2D,
    /// Most terminals don't have perfectly square characters. The value you set here is how much the final image will be stretched in the X axis to account for this. The default value is `2.2` but it will be different in most terminals
    pub character_width_multiplier: f64,
}

impl Viewport {
    pub fn new(transform: Transform3D, fov: f64, origin: Vec2D) -> Self {
        Self {
            transform,
            fov,
            origin,
            character_width_multiplier: 2.2,
        }
    }

    pub fn perspective(&self, pos: Vec3D) -> Vec2D {
        let f = self.fov / -pos.z;
        let (sx, sy) = (-pos.x * f, pos.y * f);

        // adjust for non-square pixels
        let sx = (sx * self.character_width_multiplier).round();
        let sy = sy.round();

        self.origin + Vec2D::new(sx as isize, sy as isize)
    }

    /// Return the object's vertices, transformed
    pub fn transform_vertices(&self, object: &impl ViewElement3D) -> Vec<Vec3D> {
        object
            .get_vertices()
            .iter()
            .map(|v| (self.transform * object.get_transform()) * *v)
            .collect()
    }

    /// Return the screen coordinates and distance from the view for each vertex, as parallel vectors
    pub fn get_vertices_on_screen(&self, object: &impl ViewElement3D) -> (Vec<Vec2D>, Vec<f64>) {
        self.transform_vertices(object)
            .iter()
            .map(|vertex| (self.perspective(*vertex), vertex.z))
            .unzip()
    }

    pub fn project_faces(
        &self,
        objects: Vec<&impl ViewElement3D>,
        sort_faces: bool,
        backface_culling: bool,
    ) -> Vec<(Vec<Vec2D>, ColChar)> {
        let mut screen_faces = vec![];

        for object in objects {
            let (screen_coordinates, vertex_depths) = self.get_vertices_on_screen(object);

            for face in object.get_faces().iter() {
                let face_vertices = face.index_into(&screen_coordinates);

                // Backface culling
                if !utils::is_clockwise(&face_vertices) && backface_culling {
                    continue;
                }

                let mean_z = match sort_faces {
                    true => Some(
                        face.index_into(&vertex_depths).into_iter().sum::<f64>()
                            / face_vertices.len() as f64,
                    ),
                    false => None,
                };

                screen_faces.push((face_vertices, face.fill_char, mean_z));
            }
        }

        if sort_faces {
            screen_faces.sort_by_key(|k| (k.2.unwrap() * -100.0).round() as i64);
        }

        screen_faces.into_iter().map(|(vs, c, _)| (vs, c)).collect()
    }

    pub fn render(
        &self,
        objects: Vec<&impl ViewElement3D>,
        display_mode: DisplayMode,
    ) -> PixelContainer {
        let mut canvas = PixelContainer::new();

        match display_mode {
            DisplayMode::Debug => {
                for object in objects {
                    for (i, screen_coordinates) in
                        self.get_vertices_on_screen(object).0.iter().enumerate()
                    {
                        let index_text = i.to_string();
                        canvas.blit(&Text::new(*screen_coordinates, &index_text, Modifier::None));
                    }
                }
            }
            DisplayMode::Points { fill_char } => {
                for object in objects {
                    for screen_coordinates in self.get_vertices_on_screen(object).0 {
                        canvas.push(Point::new(screen_coordinates, fill_char));
                    }
                }
            }
            DisplayMode::Wireframe { backface_culling } => {
                let screen_faces = self.project_faces(objects, false, backface_culling);

                for (face_vertices, fill_char) in screen_faces {
                    for fi in 0..face_vertices.len() {
                        let (i0, i1) = (
                            face_vertices[fi],
                            face_vertices[(fi + 1) % face_vertices.len()],
                        );
                        canvas.append_points(Line::draw(i0, i1), fill_char);
                    }
                }
            }
            DisplayMode::Solid => {
                let screen_faces = self.project_faces(objects, true, true);

                for (face_vertices, fill_char) in screen_faces {
                    canvas.append_points(Polygon::draw(&face_vertices), fill_char)
                }
            }
        }

        canvas
    }
}

pub trait ViewElement3D {
    /// This should return the object's transform
    fn get_transform(&self) -> Transform3D;
    /// This should return all of the object's vertices
    fn get_vertices(&self) -> &[Vec3D];
    /// This should return all of the object's `Face`s
    fn get_faces(&self) -> &[Face];
}