rsille/extra/
object3d.rs

1use crate::{
2    canvas::Paint,
3    utils::{check_zoom, mean, RsilleErr, MIN_DIFFERENCE},
4    Canvas,
5};
6
7use crate::color::Color;
8use std::collections::HashMap;
9
10/// A paintable Object in 3D
11///
12/// Make object and easy to do something like rotate, zoom and more
13/// also, it support colorful output
14///
15/// ## Example
16///
17/// make a cube and rotate it endlessly
18/// ```no_run
19/// use rsille::{extra::Object3D, Animation};
20/// let cube = Object3D::cube(30.0);
21/// let mut anime = Animation::new();
22/// anime.push(cube, |cube| {
23///     cube.rotate((1.0, 2.0, 3.0));
24///     false
25/// }, (0, 0));
26/// anime.run();
27/// ```
28/// also try to not paint at *(0,0)*, at other location like *(30, -30)*
29#[derive(Debug, Clone)]
30pub struct Object3D {
31    origin_vertices: Vec<Point3D>,
32    zoomed_vertices: Option<Vec<Point3D>>,
33    center: Point3D,
34    sides: HashMap<(usize, usize), Color>,
35}
36
37impl Object3D {
38    /// Construct a new Object3D, with no vertices and sides
39    pub fn new() -> Self {
40        Self {
41            origin_vertices: Vec::new(),
42            zoomed_vertices: None,
43            center: Point3D::new(0.0, 0.0, 0.0),
44            sides: HashMap::new(),
45        }
46    }
47
48    /// Return the vertices
49    pub fn vertices(&self) -> Vec<(f64, f64, f64)> {
50        self.origin_vertices.iter().map(|p| p.get()).collect()
51    }
52
53    /// Add vertices to object
54    pub fn add_points(&mut self, points: &[(f64, f64, f64)]) {
55        for p in points {
56            self.origin_vertices.push(Point3D::from(*p));
57        }
58        self.calc_center();
59    }
60
61    /// Return the sides
62    pub fn sides(&self) -> Vec<(usize, usize)> {
63        self.sides.keys().cloned().collect()
64    }
65
66    /// Add sides to object
67    ///
68    /// For example, there is 3 vertices in the object,
69    /// you want to connect the first and the second, then it's `[0, 1]`.
70    ///
71    /// Return an error if the index is out of range, like only 3 vertices but you want to connect `[0, 4]`
72    pub fn add_sides(&mut self, sides: &[(usize, usize)]) -> Result<(), RsilleErr> {
73        let vn = self.origin_vertices.len();
74        for side in sides {
75            if vn <= side.0 || vn <= side.1 {
76                return Err(RsilleErr::new("wrong add sides!".to_string()));
77            }
78
79            self.sides.insert(*side, Color::Reset);
80        }
81        Ok(())
82    }
83
84    /// Add sides and color of those sides
85    ///
86    /// Take a look at [`add_sides`](struct.Object3D.html#method.add_sides) for more information
87    pub fn add_sides_colorful(
88        &mut self,
89        sides: &[((usize, usize), Color)],
90    ) -> Result<(), RsilleErr> {
91        let vn = self.origin_vertices.len();
92        for (side, color) in sides {
93            if vn <= side.0 || vn <= side.1 {
94                return Err(RsilleErr::new("wrong add sides!".to_string()));
95            }
96            self.sides.insert(*side, *color);
97        }
98        Ok(())
99    }
100
101    /// Set the color of the side
102    ///
103    /// If there isn't the side, it will do nothing
104    pub fn set_side_colorful(&mut self, side: (usize, usize), color: Color) {
105        if self.sides.contains_key(&side) {
106            self.sides.insert(side, color);
107        }
108    }
109
110    /// Rotate the whole object
111    ///
112    /// Normaly, the rotate won't grow the error of f64.
113    /// If the error is growing, use [`rotate_new`](struct.Object3D.html#method.rotate_new)
114    pub fn rotate(&mut self, angle: (f64, f64, f64)) {
115        for p in &mut self.origin_vertices {
116            p.rotate(angle);
117        }
118    }
119
120    /// Rotate the whole object
121    ///
122    /// Similar to [`rotate`](struct.Object3D.html#method.rotate) but will return a new Object3D
123    pub fn rotate_new(&self, angle: (f64, f64, f64)) -> Self {
124        let mut obj = self.clone();
125        obj.rotate(angle);
126        obj
127    }
128
129    /// Zoom the whole object
130    ///
131    /// * `factor` - magnification of zoom, must bigger than 0.001
132    ///
133    /// Because of the implementation, it won't grow the error of f64 in most time.
134    /// But if after many times call it, the error is grow, consider use [`zoom_new`](struct.Object3D.html#method.zoom_new)
135    pub fn zoom(&mut self, factor: f64) {
136        check_zoom(factor);
137        let mut vertices = self.origin_vertices.clone();
138        vertices
139            .iter_mut()
140            .for_each(|v| v.zoom(self.center, factor));
141        self.zoomed_vertices = Some(vertices);
142    }
143
144    /// Zoom the whole object
145    ///
146    /// * `factor` - magnification of zoom, must bigger than 0.001
147    ///
148    /// Instead of change the original object, it returns a new one.
149    /// It can forbide the growing of error of f64
150    pub fn zoom_new(&self, factor: f64) -> Self {
151        check_zoom(factor);
152        let mut points: Vec<Point3D> = self.origin_vertices.clone();
153        points.iter_mut().for_each(|v| v.zoom(self.center, factor));
154        Self {
155            origin_vertices: points,
156            zoomed_vertices: None,
157            sides: self.sides.clone(),
158            center: self.center,
159        }
160    }
161
162    /// Take a closure and run it on all vertices
163    pub fn map<F>(&mut self, f: F)
164    where
165        F: Fn(f64, f64, f64) -> (f64, f64, f64),
166    {
167        for p in &mut self.origin_vertices {
168            let (x, y, z) = f(p.x, p.y, p.z);
169            p.x = x;
170            p.y = y;
171            p.z = z;
172        }
173    }
174
175    fn calc_center(&mut self) {
176        let (mut xs, mut ys, mut zs) = (Vec::new(), Vec::new(), Vec::new());
177        for p in &self.origin_vertices {
178            xs.push(p.x);
179            ys.push(p.y);
180            zs.push(p.z);
181        }
182        let (mut mx, mut my, mut mz) = (mean(&xs), mean(&ys), mean(&zs));
183
184        // forbide the lost of f64
185        if (mx - self.center.x) < MIN_DIFFERENCE {
186            mx = self.center.x;
187        }
188        if (my - self.center.y) < MIN_DIFFERENCE {
189            my = self.center.y;
190        }
191        if (mz - self.center.z) < MIN_DIFFERENCE {
192            mz = self.center.z;
193        }
194        self.center = Point3D::new(mx, my, mz);
195    }
196}
197
198impl Paint for Object3D {
199    fn paint<T>(&self, canvas: &mut Canvas, x: T, y: T) -> Result<(), RsilleErr>
200    where
201        T: Into<f64>,
202    {
203        let (x, y) = (x.into(), y.into());
204        let points = if let Some(p) = &self.zoomed_vertices {
205            p
206        } else {
207            &self.origin_vertices
208        };
209
210        for (side, color) in &self.sides {
211            let (v1, v2) = (points[side.0], points[side.1]);
212            let xy1 = (x + v1.x, y + v1.z);
213            let xy2 = (x + v2.x, y + v2.z);
214            canvas.line_colorful(xy1, xy2, *color);
215        }
216
217        Ok(())
218    }
219}
220
221impl Object3D {
222    /// Make a cube
223    pub fn cube<T>(side_len: T) -> Object3D
224    where
225        T: Into<f64>,
226    {
227        let side_len = side_len.into();
228        let mut object = Object3D::new();
229        #[rustfmt::skip]
230        // the vertices of cube
231        let a = [
232            (-1, -1, -1),
233            (-1, -1,  1),
234            (-1,  1, -1),
235            ( 1, -1, -1),
236            (-1,  1,  1),
237            ( 1, -1,  1),
238            ( 1,  1, -1),
239            ( 1,  1,  1),
240        ];
241        let mut points = Vec::new();
242        for i in a {
243            let x = side_len / 2.0 * i.0 as f64;
244            let y = side_len / 2.0 * i.1 as f64;
245            let z = side_len / 2.0 * i.2 as f64;
246            points.push((x, y, z));
247        }
248        object.add_points(&points);
249        object
250            .add_sides(&[
251                (0, 1),
252                (1, 4),
253                (4, 2),
254                (2, 0),
255                (3, 5),
256                (5, 7),
257                (7, 6),
258                (6, 3),
259                (1, 5),
260                (4, 7),
261                (2, 6),
262                (0, 3),
263            ])
264            .unwrap();
265        object
266    }
267}
268
269/// point in 3D
270///
271/// support rotate and zoom, not paintable
272#[derive(Debug, Clone, Copy)]
273struct Point3D {
274    pub(crate) x: f64,
275    pub(crate) y: f64,
276    pub(crate) z: f64,
277}
278
279#[allow(unused)]
280impl Point3D {
281    /// construct a new point
282    fn new(x: f64, y: f64, z: f64) -> Self {
283        Self { x, y, z }
284    }
285
286    /// similar to new, but use a tuple
287    fn from(xyz: (f64, f64, f64)) -> Self {
288        Self {
289            x: xyz.0,
290            y: xyz.1,
291            z: xyz.2,
292        }
293    }
294
295    /// get the coordinate
296    fn get(&self) -> (f64, f64, f64) {
297        (self.x, self.y, self.z)
298    }
299
300    /// rotate the point, see [wiki] for more information
301    ///
302    /// [wiki]: <https://en.wikipedia.org/wiki/Rotation_matrix>
303    fn rotate(&mut self, angle: (f64, f64, f64)) {
304        self.rotate_x(angle.0);
305        self.rotate_y(angle.1);
306        self.rotate_z(angle.2);
307
308        // let (x, y, z) = (self.x, self.y, self.z);
309        // let (sx, cx) = anlge_x.to_radians().sin_cos();
310        // let (sy, cy) = anlge_y.to_radians().sin_cos();
311        // let (sz, cz) = angle_z.to_radians().sin_cos();
312        // let (t1, t2, t3) = (
313        //     x * cy + y * sx * sy + z * cx * sy,
314        //     y * cx - z * sx,
315        //     y * sx + z * cx,
316        // );
317        // self.x = cz * t1 - sz * t2;
318        // self.y = sz * t1 + cz * t2;
319        // self.z = cz * t3 - sy * x;
320    }
321
322    /// similar to [rotate] but don't change the original point and return the rotated point
323    ///
324    /// [rotate]: struct.Point3D.html#method.rotate
325    fn rotate_new(&self, angle: (f64, f64, f64)) -> Self {
326        let mut point = *self;
327        point.rotate(angle);
328        point
329    }
330
331    /// Zoom coordinate of the point with the center
332    ///
333    /// It will change the coordinate, so don't call it many times, precision errors of f64 are cumulative!
334    ///
335    /// In most time, you shouldn't use it, just use the [`zoom`](struct.Object3D.html#method.zoom) in Object3D
336    fn zoom(&mut self, center: Self, factor: f64) {
337        check_zoom(factor);
338        self.x = (self.x - center.x) * factor;
339        self.y = (self.y - center.y) * factor;
340        self.z = (self.z - center.z) * factor;
341    }
342
343    /// Zoom coordinate of the point with the center and return a new point
344    ///
345    /// It won't change the original point, so the precision error isn't matter
346    fn zoom_new(&self, center: Self, factor: f64) -> Point3D {
347        check_zoom(factor);
348        let dx = (self.x - center.x) * factor;
349        let dy = (self.y - center.y) * factor;
350        let dz = (self.z - center.y) * factor;
351
352        Self {
353            x: dx,
354            y: dy,
355            z: dz,
356        }
357    }
358
359    /// rotate by an angle about the x axis
360    fn rotate_x(&mut self, angle: f64) {
361        let (s, c) = angle.to_radians().sin_cos();
362        let (y, z) = (self.y, self.z);
363        self.y = y * c - z * s;
364        self.z = y * s + z * c;
365    }
366
367    /// rotate by an angle about the y axis
368    fn rotate_y(&mut self, angle: f64) {
369        let (s, c) = angle.to_radians().sin_cos();
370        let (x, z) = (self.x, self.z);
371        self.x = x * c + z * s;
372        self.z = -x * s + z * c;
373    }
374
375    /// rotate by an angle about the z axis
376    fn rotate_z(&mut self, anlge: f64) {
377        let (s, c) = anlge.to_radians().sin_cos();
378        let (x, y) = (self.x, self.y);
379        self.x = x * c - y * s;
380        self.y = x * s + y * c;
381    }
382}