Skip to main content

mkmidilibrary/render/
elements.rs

1//! Score element - the top-level rendering element
2
3use std::any::Any;
4
5use mkgraphic::prelude::*;
6use mkgraphic::support::canvas::Canvas;
7use num::ToPrimitive;
8
9use super::clef::ClefElement;
10use super::config::RenderConfig;
11use super::staff::StaffElement;
12use super::{STAFF_HEIGHT, STAFF_SPACE};
13use crate::notation::Clef;
14use crate::stream::Score;
15
16/// A graphical element representing an entire score
17pub struct ScoreElement {
18    /// The score data
19    parts_data: Vec<PartData>,
20    /// Rendering configuration (kept for future use)
21    #[allow(dead_code)]
22    config: RenderConfig,
23    /// Total width
24    width: f32,
25    /// Total height
26    height: f32,
27}
28
29/// Data for rendering a single part
30struct PartData {
31    /// Part name (kept for future use - part labels)
32    #[allow(dead_code)]
33    name: String,
34    /// Clef for this part
35    clef: Clef,
36    /// Measure count
37    measure_count: usize,
38    /// Notes per measure (simplified representation)
39    measures: Vec<MeasureData>,
40}
41
42/// Data for a single measure
43struct MeasureData {
44    /// Note offsets and MIDI values
45    notes: Vec<(f64, u8)>,
46}
47
48impl ScoreElement {
49    /// Create a new score element from a Score
50    pub fn new(score: &Score, config: RenderConfig) -> Self {
51        let mut parts_data = Vec::new();
52
53        for part in score.parts() {
54            // Default to treble clef (Measure doesn't store clef directly)
55            let clef = Clef::treble();
56
57            let measures: Vec<MeasureData> = part
58                .measures()
59                .iter()
60                .map(|measure| {
61                    let notes: Vec<(f64, u8)> = measure
62                        .elements()
63                        .iter()
64                        .filter_map(|(offset, elem)| {
65                            use crate::stream::MusicElement;
66                            match elem {
67                                MusicElement::Note(n) => {
68                                    Some((offset.to_f64().unwrap_or(0.0), n.midi()))
69                                }
70                                _ => None,
71                            }
72                        })
73                        .collect();
74                    MeasureData { notes }
75                })
76                .collect();
77
78            parts_data.push(PartData {
79                name: part.name().unwrap_or("Part").to_string(),
80                clef,
81                measure_count: part.measures().len(),
82                measures,
83            });
84        }
85
86        // Calculate dimensions
87        let num_parts = parts_data.len().max(1);
88        let num_measures = parts_data
89            .first()
90            .map(|p| p.measure_count)
91            .unwrap_or(0)
92            .max(1);
93
94        let width = config.margin_left
95            + config.clef_width
96            + config.key_sig_width
97            + config.time_sig_width
98            + (num_measures as f32 * config.measure_width)
99            + config.margin_right;
100
101        let staff_with_spacing = config.staff.height + config.staff_spacing;
102        let height =
103            config.margin_top + (num_parts as f32 * staff_with_spacing) + config.margin_bottom;
104
105        Self {
106            parts_data,
107            config,
108            width,
109            height,
110        }
111    }
112
113    /// Get the width
114    pub fn width(&self) -> f32 {
115        self.width
116    }
117
118    /// Get the height
119    pub fn height(&self) -> f32 {
120        self.height
121    }
122
123    /// Draw the score to a canvas
124    pub fn draw_to_canvas(&self, canvas: &mut Canvas, config: &RenderConfig) {
125        // Clear background
126        let bg = &config.colors.background;
127        canvas.clear(Color::new(bg.0, bg.1, bg.2, bg.3));
128
129        let staff_with_spacing = config.staff.height + config.staff_spacing;
130
131        // Draw each part
132        for (part_idx, part) in self.parts_data.iter().enumerate() {
133            let staff_y =
134                config.margin_top + STAFF_HEIGHT / 2.0 + (part_idx as f32 * staff_with_spacing);
135
136            // Draw staff lines
137            let staff_width = self.width - config.margin_left - config.margin_right;
138            let mut staff = StaffElement::new(staff_width);
139            staff.set_position(config.margin_left, staff_y);
140            staff.draw_to_canvas(canvas, &config.colors.staff_lines);
141
142            // Draw clef
143            let mut clef_element = ClefElement::new(part.clef);
144            clef_element.set_position(config.margin_left + 5.0, staff_y);
145            clef_element.draw_to_canvas(canvas, config);
146
147            // Draw measures
148            let measure_start_x = config.margin_left
149                + config.clef_width
150                + config.key_sig_width
151                + config.time_sig_width;
152
153            for (measure_idx, measure_data) in part.measures.iter().enumerate() {
154                let measure_x = measure_start_x + (measure_idx as f32 * config.measure_width);
155                let is_last = measure_idx == part.measure_count - 1;
156
157                // Draw notes in this measure
158                for (offset, midi) in &measure_data.notes {
159                    let note_x = measure_x + (*offset as f32 * config.measure_width * 0.8);
160                    let position = super::midi_to_staff_position(*midi, &part.clef);
161                    let note_y = staff_y + position.to_y(STAFF_SPACE);
162
163                    // Draw simple note head
164                    self.draw_simple_note(canvas, note_x, note_y, config);
165
166                    // Draw ledger lines if needed
167                    if position.position > 4 || position.position < -4 {
168                        staff.draw_ledger_lines(
169                            canvas,
170                            position.position,
171                            note_x,
172                            config.note.head_width,
173                            &config.colors.staff_lines,
174                        );
175                    }
176                }
177
178                // Draw bar line
179                let bar_x = measure_x + config.measure_width;
180                let top_y = staff_y - STAFF_HEIGHT / 2.0;
181                let bottom_y = staff_y + STAFF_HEIGHT / 2.0;
182
183                if is_last {
184                    super::staff::draw_double_bar_line(
185                        canvas,
186                        bar_x - 6.0,
187                        top_y,
188                        bottom_y,
189                        (1.0, 3.0, 4.0),
190                        &config.colors.bar_lines,
191                    );
192                } else {
193                    super::staff::draw_bar_line(
194                        canvas,
195                        bar_x,
196                        top_y,
197                        bottom_y,
198                        1.0,
199                        &config.colors.bar_lines,
200                    );
201                }
202
203                // Draw measure number
204                if config.show_bar_numbers && measure_idx == 0 {
205                    // Would draw measure number here with text rendering
206                }
207            }
208
209            // Draw initial bar line
210            let initial_bar_x = measure_start_x;
211            let top_y = staff_y - STAFF_HEIGHT / 2.0;
212            let bottom_y = staff_y + STAFF_HEIGHT / 2.0;
213            super::staff::draw_bar_line(
214                canvas,
215                initial_bar_x,
216                top_y,
217                bottom_y,
218                1.0,
219                &config.colors.bar_lines,
220            );
221        }
222    }
223
224    /// Draw a simple note (filled oval)
225    fn draw_simple_note(&self, canvas: &mut Canvas, x: f32, y: f32, config: &RenderConfig) {
226        let colors = &config.colors.notes;
227        let color = Color::new(colors.0, colors.1, colors.2, colors.3);
228
229        canvas.fill_style(color);
230        canvas.begin_path();
231        canvas.add_circle(mkgraphic::support::circle::Circle::new(
232            Point::new(x + config.note.head_width / 2.0, y),
233            config.note.head_height / 2.0 * 0.9,
234        ));
235        canvas.fill();
236
237        // Draw stem
238        canvas.stroke_style(color);
239        canvas.line_width(config.note.stem_width);
240        canvas.begin_path();
241        let stem_x = x + config.note.head_width;
242        canvas.move_to(Point::new(stem_x, y));
243        canvas.line_to(Point::new(stem_x, y - config.note.stem_height));
244        canvas.stroke();
245    }
246}
247
248impl Element for ScoreElement {
249    fn limits(&self, _ctx: &BasicContext) -> ViewLimits {
250        ViewLimits::fixed(self.width, self.height)
251    }
252
253    fn draw(&self, _ctx: &Context) {
254        // Note: In mkgraphic, drawing typically happens through the Context
255        // For now, actual drawing is done via draw_to_canvas
256    }
257
258    fn as_any(&self) -> &dyn Any {
259        self
260    }
261
262    fn as_any_mut(&mut self) -> &mut dyn Any {
263        self
264    }
265}
266
267/// Helper to create a canvas and render a score
268pub fn render_score_to_image(score: &Score, config: &RenderConfig) -> Option<Vec<u8>> {
269    let element = ScoreElement::new(score, config.clone());
270    let width = (element.width() * config.scale) as u32;
271    let height = (element.height() * config.scale) as u32;
272
273    let mut canvas = Canvas::new(width, height)?;
274
275    if config.scale != 1.0 {
276        canvas.scale(config.scale, config.scale);
277    }
278
279    element.draw_to_canvas(&mut canvas, config);
280
281    // Return PNG data
282    canvas.pixmap().encode_png().ok()
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_score_element_creation() {
291        let score = Score::new();
292        let config = RenderConfig::default();
293        let element = ScoreElement::new(&score, config);
294
295        assert!(element.width() > 0.0);
296        assert!(element.height() > 0.0);
297    }
298}