Skip to main content

mraphics_mobject/two_d/
mobject_2d.rs

1use mraphics_core::{
2    Color, GadgetIndex, GeometryView, InstanceUpdater, Material, MeshLike, Mobject2DMaterial,
3    MraphicsID, MultiColoredMaterial, RenderInstance,
4};
5use nalgebra::{UnitVector3, Vector3};
6
7const PREVIOUS_ATTR_LABEL: &'static str = "mobject-2d-previous-attribute";
8const REVERSE_ATTR_LABEL: &'static str = "mobject-2d-reverse-attribute";
9const THICKNESS_LABEL: &'static str = "mobject-2d-thickness-uniform";
10
11#[derive(Debug)]
12pub struct Mobject2DPath {
13    pub vertices: Vec<[f32; 3]>,
14
15    pub stroked: bool,
16    pub filled: bool,
17
18    pub stroke_color: Color<f32>,
19    pub fill_color: Color<f32>,
20}
21
22impl Mobject2DPath {
23    pub fn new(vertices: Vec<[f32; 3]>) -> Self {
24        Self {
25            vertices,
26
27            stroked: false,
28            filled: false,
29
30            // SAFETY: `RED` and `WHITE` are valid hex color strings defined in `crate::constants`.
31            // `Color::from_hex_str` will succeed without panicking for these well-formed inputs.
32            stroke_color: Color::from_hex_str(mraphics_core::constants::RED).unwrap(),
33            fill_color: Color::from_hex_str(mraphics_core::constants::WHITE).unwrap(),
34        }
35    }
36}
37
38pub struct Mobject2DStroke {
39    pub color: Color<f32>,
40    pub thickness: f32,
41    material: Mobject2DMaterial,
42}
43
44impl Mobject2DStroke {
45    fn new() -> Self {
46        Self {
47            // SAFETY: `RED` is a valid hex color string defined in `crate::constants`.
48            color: Color::from_hex_str(mraphics_core::constants::RED).unwrap(),
49            thickness: 0.05,
50            material: Mobject2DMaterial::new(),
51        }
52    }
53
54    fn init_geometry_view(&self, view: &mut GeometryView) {
55        view.add_attribute(
56            mraphics_core::constants::POSITION_ATTR_LABEL,
57            mraphics_core::constants::POSITION_ATTR_INDEX,
58            vec![],
59        );
60
61        view.add_attribute(
62            PREVIOUS_ATTR_LABEL,
63            GadgetIndex {
64                group_index: 1,
65                binding_index: 3,
66            },
67            vec![],
68        );
69
70        view.add_attribute(
71            REVERSE_ATTR_LABEL,
72            GadgetIndex {
73                group_index: 1,
74                binding_index: 4,
75            },
76            vec![],
77        );
78
79        view.add_attribute(
80            mraphics_core::constants::COLOR_ATTR_LABEL,
81            mraphics_core::constants::COLOR_ATTR_INDEX,
82            vec![],
83        );
84
85        view.add_uniform(
86            THICKNESS_LABEL,
87            GadgetIndex {
88                group_index: 1,
89                binding_index: 5,
90            },
91            vec![],
92        );
93    }
94
95    fn update_instance(&self, paths: &Vec<Mobject2DPath>, instance: &mut RenderInstance) {
96        let view = &mut instance.geometry;
97
98        let mut vertices = Vec::new();
99        let mut previous = Vec::new();
100        let mut color = Vec::new();
101        let mut reverse = Vec::new();
102
103        fn to_homogeneous(point: &[f32; 3]) -> [f32; 4] {
104            [point[0], point[1], point[2], 1.0]
105        }
106
107        let mut build_path = |path: &Mobject2DPath| {
108            let points = &path.vertices;
109
110            // We need at least two points to build segments.
111            if points.is_empty() || points.len() < 2 {
112                return;
113            }
114
115            for i in 1..points.len() {
116                let start = &to_homogeneous(&points[i]);
117                let end = &to_homogeneous(&points[i - 1]);
118
119                vertices.extend_from_slice(start);
120                vertices.extend_from_slice(start);
121                vertices.extend_from_slice(end);
122                vertices.extend_from_slice(start);
123                vertices.extend_from_slice(end);
124                vertices.extend_from_slice(end);
125
126                previous.extend_from_slice(end);
127                previous.extend_from_slice(end);
128                previous.extend_from_slice(start);
129                previous.extend_from_slice(end);
130                previous.extend_from_slice(start);
131                previous.extend_from_slice(start);
132
133                color.extend_from_slice(&path.stroke_color);
134                color.extend_from_slice(&path.stroke_color);
135                color.extend_from_slice(&path.stroke_color);
136                color.extend_from_slice(&path.stroke_color);
137                color.extend_from_slice(&path.stroke_color);
138                color.extend_from_slice(&path.stroke_color);
139
140                reverse.extend_from_slice(&[-1., 1., 1., 1., 1., -1.]);
141            }
142        };
143
144        for path in paths {
145            if path.stroked {
146                build_path(&path);
147            }
148        }
149
150        // SAFETY: These attributes exist because we initialized them in `Self::init_geometry_view`
151        view.set_attribute(
152            mraphics_core::constants::POSITION_ATTR_LABEL,
153            Vec::from(bytemuck::cast_slice::<f32, u8>(&vertices)),
154        )
155        .unwrap();
156        view.set_attribute(
157            PREVIOUS_ATTR_LABEL,
158            Vec::from(bytemuck::cast_slice::<f32, u8>(&previous)),
159        )
160        .unwrap();
161        view.set_attribute(
162            REVERSE_ATTR_LABEL,
163            Vec::from(bytemuck::cast_slice::<f32, u8>(&reverse)),
164        )
165        .unwrap();
166        view.set_attribute(
167            mraphics_core::constants::COLOR_ATTR_LABEL,
168            Vec::from(bytemuck::cast_slice::<f32, u8>(&color)),
169        )
170        .unwrap();
171        view.set_uniform(
172            THICKNESS_LABEL,
173            Vec::from(bytemuck::cast_slice::<f32, u8>(&[self.thickness])),
174        )
175        .unwrap();
176
177        let vertex_count = (vertices.len() / 4) as u32;
178
179        view.indices = mraphics_core::GeometryIndices::Sequential(vertex_count);
180
181        if vertex_count == 0 {
182            instance.visible = false;
183        } else {
184            instance.visible = true;
185        }
186    }
187}
188
189pub struct Mobject2DFill {
190    pub color: Color<f32>,
191    material: MultiColoredMaterial,
192}
193
194impl Mobject2DFill {
195    fn new() -> Self {
196        Self {
197            // SAFETY: `WHITE` is a valid hex color string defined in `crate::constants`.
198            color: Color::from_hex_str(mraphics_core::constants::WHITE).unwrap(),
199
200            material: MultiColoredMaterial::new(),
201        }
202    }
203
204    fn init_geometry_view(&self, view: &mut GeometryView) {
205        view.add_attribute(
206            mraphics_core::constants::POSITION_ATTR_LABEL,
207            mraphics_core::constants::POSITION_ATTR_INDEX,
208            vec![],
209        );
210        view.add_attribute(
211            mraphics_core::constants::COLOR_ATTR_LABEL,
212            mraphics_core::constants::COLOR_ATTR_INDEX,
213            vec![],
214        );
215    }
216
217    fn update_instance(&self, paths: &Vec<Mobject2DPath>, instance: &mut RenderInstance) {
218        let view = &mut instance.geometry;
219
220        let mut vertices = Vec::new();
221        let mut colors = Vec::new();
222
223        fn to_homogeneous(point: &[f32; 3]) -> [f32; 4] {
224            [point[0], point[1], point[2], 1.0]
225        }
226
227        fn build_path(path: &Mobject2DPath, vertices: &mut Vec<f32>, colors: &mut Vec<f32>) {
228            let points = &path.vertices;
229
230            // We need at least three points to build polygons.
231            if points.is_empty() || points.len() < 3 {
232                return;
233            }
234
235            let first = &points[0];
236
237            // SAFETY: Indices are within the valid range.
238            // 1. Range `1..(points.len() - 1)` ensures `i ∈ [1, len - 2]`
239            // 2. Thus `i < len` and `i + 1 < len` for all iterations
240            for i in 1..(points.len() - 1) {
241                vertices.extend_from_slice(&to_homogeneous(first));
242                vertices.extend_from_slice(&to_homogeneous(&points[i]));
243                vertices.extend_from_slice(&to_homogeneous(&points[i + 1]));
244
245                colors.extend_from_slice(&path.fill_color);
246                colors.extend_from_slice(&path.fill_color);
247                colors.extend_from_slice(&path.fill_color);
248            }
249        }
250
251        for path in paths {
252            if path.filled {
253                build_path(&path, &mut vertices, &mut colors);
254            }
255        }
256
257        // SAFETY: These attributes exist because we initialized them in `Self::init_geometry_view`
258        view.set_attribute(
259            mraphics_core::constants::POSITION_ATTR_LABEL,
260            Vec::from(bytemuck::cast_slice::<f32, u8>(&vertices)),
261        )
262        .unwrap();
263        view.set_attribute(
264            mraphics_core::constants::COLOR_ATTR_LABEL,
265            Vec::from(bytemuck::cast_slice::<f32, u8>(&colors)),
266        )
267        .unwrap();
268
269        let vertex_count = (vertices.len() / 4) as u32;
270
271        view.indices = mraphics_core::GeometryIndices::Sequential(vertex_count);
272
273        if vertex_count == 0 {
274            instance.visible = false;
275        } else {
276            instance.visible = true;
277        }
278    }
279}
280
281/// Describes an arc.
282///
283/// The arc is defined on a plane spanned by `x_axis` and `y_axis`.
284/// The arc spans from `start_rad` to `end_rad` radians,
285/// with direction controlled by `clockwise`.
286/// The path is discretized into `segment_num` line segments.
287pub struct Mobject2DArcDescriptor {
288    pub radius: f32,
289    pub start_rad: f32,
290    pub end_rad: f32,
291    pub clockwise: bool,
292    pub segment_num: u32,
293
294    pub center: [f32; 3],
295
296    /// Local X-axis of the arc's plane, defining 0 radians direction.
297    pub x_axis: UnitVector3<f32>,
298
299    /// Local Y-axis of the arc's plane.
300    pub y_axis: UnitVector3<f32>,
301}
302
303impl Default for Mobject2DArcDescriptor {
304    fn default() -> Self {
305        Self {
306            radius: 1.0,
307            start_rad: 0.0,
308            end_rad: std::f32::consts::PI,
309            clockwise: false,
310            segment_num: 25,
311
312            center: [0.0, 0.0, 0.0],
313
314            x_axis: UnitVector3::new_normalize(Vector3::x()),
315            y_axis: UnitVector3::new_normalize(Vector3::y()),
316        }
317    }
318}
319
320pub struct Mobject2D {
321    identifier: MraphicsID,
322
323    vertices: Vec<[f32; 3]>,
324    pub paths: Vec<Mobject2DPath>,
325
326    pub stroke: Mobject2DStroke,
327    pub fill: Mobject2DFill,
328}
329
330impl Mobject2D {
331    pub fn new() -> Self {
332        Self {
333            identifier: MraphicsID::acquire(),
334
335            vertices: Vec::new(),
336            paths: Vec::new(),
337
338            stroke: Mobject2DStroke::new(),
339            fill: Mobject2DFill::new(),
340        }
341    }
342
343    /// Moves the current path point to the specified position.
344    ///
345    /// This operation ends the previous path (if any) and starts a new path
346    /// beginning at the given point. If there was an active path with vertices,
347    /// it will be finalized before starting the new path.
348    pub fn move_to(&mut self, point: [f32; 3]) {
349        self.finish();
350
351        self.vertices.push(point);
352    }
353
354    /// Draws a straight line from the previous path point to the specified point.
355    ///
356    /// The line is added to the current active path.
357    /// If no path is active or there is no previous path point,
358    /// this operation will behave like [`Self::move_to`].
359    pub fn line_to(&mut self, point: [f32; 3]) {
360        self.vertices.push(point);
361    }
362
363    /// Draws an arc.
364    ///
365    /// This operation ends the previous path (if any) and inserts the arc.
366    ///
367    /// # Arguments
368    /// - desc: Arc specification. See [`Mobject2DArcDescriptor`] for details.
369    pub fn arc(&mut self, desc: &Mobject2DArcDescriptor) {
370        self.finish();
371
372        let &Mobject2DArcDescriptor {
373            radius,
374            mut start_rad,
375            mut end_rad,
376            mut clockwise,
377            segment_num,
378            center,
379            x_axis,
380            y_axis,
381        } = desc;
382
383        if radius == 0.0 || start_rad == end_rad {
384            return;
385        }
386
387        let center = Vector3::from_column_slice(&center);
388
389        if start_rad > end_rad {
390            start_rad = end_rad;
391            end_rad = desc.start_rad;
392            clockwise = !clockwise;
393        }
394
395        let unit = if clockwise {
396            (end_rad - start_rad - std::f32::consts::PI * 2.0) / segment_num as f32
397        } else {
398            (end_rad - start_rad) / segment_num as f32
399        };
400
401        let (x_axis, y_axis) = (x_axis.into_inner(), y_axis.into_inner());
402
403        for i in 0..=segment_num {
404            let angle = start_rad + i as f32 * unit;
405            let position = center + x_axis * angle.cos() * radius + y_axis * angle.sin() * radius;
406            self.vertices.push([position[0], position[1], position[2]]);
407        }
408    }
409
410    /// Strokes the most recently drawn path.
411    ///
412    /// This method finalizes the current path (if any) and marks it for
413    /// stroking with the current stroke color.
414    pub fn stroke(&mut self) {
415        self.finish();
416
417        let len = self.paths.len();
418
419        if len == 0 {
420            return;
421        }
422
423        self.paths[len - 1].stroked = true;
424        self.paths[len - 1].stroke_color = self.stroke.color.clone();
425    }
426
427    /// Fills the most recently drawn path.
428    ///
429    /// This method finalizes the current path (if any) and marks it for
430    /// filling with the current fill color.
431    pub fn fill(&mut self) {
432        self.finish();
433
434        let len = self.paths.len();
435
436        if len == 0 {
437            return;
438        }
439
440        self.paths[len - 1].filled = true;
441        self.paths[len - 1].fill_color = self.fill.color.clone();
442    }
443
444    /// Finalizes the current drawing path.
445    ///
446    /// This method is automatically called by [`Self::stroke`] and [`Self::fill`] methods,
447    /// but can also be called manually when you want to finalize a path
448    /// without applying stroke or fill.
449    pub fn finish(&mut self) {
450        if self.vertices.len() == 0 {
451            return;
452        }
453
454        self.paths
455            .push(Mobject2DPath::new(std::mem::take(&mut self.vertices)));
456    }
457}
458
459impl InstanceUpdater for Mobject2D {
460    fn update_instance(&self, instance: &mut RenderInstance) {
461        self.fill.update_instance(&self.paths, instance);
462
463        // SAFETY: `instance.children[0]` is initialized because:
464        // 1. `Self::build_instance` ensures the first child exists and is properly initialized
465        // 2. The `instance` structure maintains this invariant throughout its lifetime
466        self.stroke
467            .update_instance(&self.paths, &mut instance.children[0]);
468    }
469}
470
471impl MeshLike for Mobject2D {
472    fn identifier(&self) -> MraphicsID {
473        self.identifier
474    }
475
476    fn build_instance(&self) -> mraphics_core::RenderInstance {
477        let mut instance = RenderInstance::new(self.identifier, &self.fill.material);
478        let mut stroke = RenderInstance::new(MraphicsID::acquire(), &self.stroke.material);
479
480        self.fill.init_geometry_view(&mut instance.geometry);
481        self.stroke.init_geometry_view(&mut stroke.geometry);
482
483        self.fill.material.update_view(&mut instance.material);
484        self.stroke.material.update_view(&mut stroke.material);
485
486        instance.add_child(stroke);
487
488        instance
489    }
490}