1use crate::{LayoutConfig, compute_layout};
2use acorde_core::Score;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
7pub enum PaperSize {
8 A4,
9 Letter,
10 Legal,
11 Custom { width_mm: f32, height_mm: f32 },
12}
13
14impl PaperSize {
15 fn dimensions_mm(self) -> (f32, f32) {
16 match self {
17 Self::A4 => (210.0, 297.0),
18 Self::Letter => (215.9, 279.4),
19 Self::Legal => (215.9, 355.6),
20 Self::Custom {
21 width_mm,
22 height_mm,
23 } => (width_mm, height_mm),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
30pub enum PageOrientation {
31 Portrait,
32 Landscape,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(default)]
41pub struct PrintConfig {
42 pub paper_size: PaperSize,
43 pub orientation: PageOrientation,
44 pub margin_top_mm: f32,
45 pub margin_right_mm: f32,
46 pub margin_bottom_mm: f32,
47 pub margin_left_mm: f32,
48 pub system_height_mm: f32,
49 pub measures_per_system: usize,
50 pub systems_per_page: Option<usize>,
53}
54
55impl Default for PrintConfig {
56 fn default() -> Self {
57 Self {
58 paper_size: PaperSize::A4,
59 orientation: PageOrientation::Portrait,
60 margin_top_mm: 16.0,
61 margin_right_mm: 14.0,
62 margin_bottom_mm: 16.0,
63 margin_left_mm: 14.0,
64 system_height_mm: 24.0,
65 measures_per_system: 4,
66 systems_per_page: None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct SystemLayout {
74 pub address: SystemAddress,
75 pub system_index: usize,
76 pub page_index: usize,
77 pub measure_indices: Vec<usize>,
78 pub top_mm: f32,
79 pub height_mm: f32,
80 pub break_reason: BreakReason,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
85pub struct PageAddress {
86 pub page_index: usize,
87}
88
89#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
91pub struct SystemAddress {
92 pub system_index: usize,
93 pub page_index: usize,
94 pub index_on_page: usize,
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
99pub enum BreakReason {
100 MeasureCapacity,
101 ExplicitSystemBreak,
102 ExplicitPageBreak,
103 PageCapacity,
104 EndOfScore,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct PageLayout {
110 pub address: PageAddress,
111 pub page_index: usize,
112 pub width_mm: f32,
113 pub height_mm: f32,
114 pub content_width_mm: f32,
115 pub content_height_mm: f32,
116 pub systems: Vec<SystemLayout>,
117 pub break_reason: BreakReason,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub struct PrintLayoutResult {
123 pub contract_version: u16,
124 pub pages: Vec<PageLayout>,
125}
126
127#[derive(Debug, thiserror::Error, PartialEq)]
128pub enum PrintLayoutError {
129 #[error("paper dimensions must be finite and greater than zero")]
130 InvalidPaperDimensions,
131 #[error("margins must be finite and non-negative")]
132 InvalidMargins,
133 #[error("system height must be finite and greater than zero")]
134 InvalidSystemHeight,
135 #[error("margins leave no usable page area")]
136 NoUsablePageArea,
137}
138
139pub fn compute_print_layout(
141 score: &Score,
142 config: &PrintConfig,
143) -> Result<PrintLayoutResult, PrintLayoutError> {
144 let (mut width_mm, mut height_mm) = config.paper_size.dimensions_mm();
145 if !width_mm.is_finite() || !height_mm.is_finite() || width_mm <= 0.0 || height_mm <= 0.0 {
146 return Err(PrintLayoutError::InvalidPaperDimensions);
147 }
148 if matches!(config.orientation, PageOrientation::Landscape) {
149 std::mem::swap(&mut width_mm, &mut height_mm);
150 }
151
152 let margins = [
153 config.margin_top_mm,
154 config.margin_right_mm,
155 config.margin_bottom_mm,
156 config.margin_left_mm,
157 ];
158 if margins
159 .iter()
160 .any(|value| !value.is_finite() || *value < 0.0)
161 {
162 return Err(PrintLayoutError::InvalidMargins);
163 }
164 if !config.system_height_mm.is_finite() || config.system_height_mm <= 0.0 {
165 return Err(PrintLayoutError::InvalidSystemHeight);
166 }
167
168 let content_width_mm = width_mm - config.margin_left_mm - config.margin_right_mm;
169 let content_height_mm = height_mm - config.margin_top_mm - config.margin_bottom_mm;
170 if content_width_mm <= 0.0 || content_height_mm <= 0.0 {
171 return Err(PrintLayoutError::NoUsablePageArea);
172 }
173
174 let systems_per_page = config
175 .systems_per_page
176 .unwrap_or_else(|| {
177 (content_height_mm / config.system_height_mm)
178 .floor()
179 .max(1.0) as usize
180 })
181 .max(1);
182 let layout = compute_layout(
183 score,
184 &LayoutConfig {
185 measures_per_row: config.measures_per_system.max(1),
186 ..LayoutConfig::default()
187 },
188 );
189
190 let mut pages = Vec::new();
191 let mut page_systems = Vec::new();
192 let mut page_index = 0;
193 for (system_index, row) in layout.rows.iter().enumerate() {
194 let explicit_page_break = row.measure_indices.last().is_some_and(|&measure_index| {
195 score
196 .parts
197 .iter()
198 .flat_map(|part| part.staves.iter())
199 .filter_map(|staff| staff.measures.get(measure_index))
200 .any(|measure| measure.page_break)
201 });
202 let explicit_system_break = row.measure_indices.last().is_some_and(|&measure_index| {
203 score
204 .parts
205 .iter()
206 .flat_map(|part| part.staves.iter())
207 .filter_map(|staff| staff.measures.get(measure_index))
208 .any(|measure| measure.system_break)
209 });
210 let is_last_system = system_index + 1 == layout.rows.len();
211 let break_reason = if explicit_page_break {
212 BreakReason::ExplicitPageBreak
213 } else if explicit_system_break {
214 BreakReason::ExplicitSystemBreak
215 } else if is_last_system {
216 BreakReason::EndOfScore
217 } else {
218 BreakReason::MeasureCapacity
219 };
220 let system = SystemLayout {
221 address: SystemAddress {
222 system_index,
223 page_index,
224 index_on_page: page_systems.len(),
225 },
226 system_index,
227 page_index,
228 measure_indices: row.measure_indices.clone(),
229 top_mm: config.margin_top_mm + page_systems.len() as f32 * config.system_height_mm,
230 height_mm: config.system_height_mm,
231 break_reason,
232 };
233 page_systems.push(system);
234
235 let page_is_full = page_systems.len() >= systems_per_page;
236 if page_is_full || explicit_page_break {
237 let page_break_reason = if explicit_page_break {
238 BreakReason::ExplicitPageBreak
239 } else if is_last_system {
240 BreakReason::EndOfScore
241 } else {
242 BreakReason::PageCapacity
243 };
244 pages.push(PageLayout {
245 address: PageAddress { page_index },
246 page_index,
247 width_mm,
248 height_mm,
249 content_width_mm,
250 content_height_mm,
251 systems: std::mem::take(&mut page_systems),
252 break_reason: page_break_reason,
253 });
254 page_index += 1;
255 }
256 }
257 if !page_systems.is_empty() || pages.is_empty() {
258 pages.push(PageLayout {
259 address: PageAddress { page_index },
260 page_index,
261 width_mm,
262 height_mm,
263 content_width_mm,
264 content_height_mm,
265 systems: page_systems,
266 break_reason: BreakReason::EndOfScore,
267 });
268 }
269
270 Ok(PrintLayoutResult {
271 contract_version: 2,
272 pages,
273 })
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use acorde_core::{Clef, Measure, Part, Score, Staff};
280
281 fn score_with_measures(count: usize) -> Score {
282 let mut score = Score::default();
283 let mut part = Part::new("Piano", "Pno.");
284 let mut staff = Staff::new(Clef::Treble);
285 staff.measures = (0..count).map(|_| Measure::empty(4, 4)).collect();
286 part.staves = vec![staff];
287 score.parts = vec![part];
288 score
289 }
290
291 #[test]
292 fn paginates_rows_and_preserves_measure_indices() {
293 let score = score_with_measures(5);
294 let result = compute_print_layout(
295 &score,
296 &PrintConfig {
297 measures_per_system: 2,
298 systems_per_page: Some(2),
299 ..PrintConfig::default()
300 },
301 )
302 .expect("valid print config");
303 assert_eq!(result.pages.len(), 2);
304 assert_eq!(
305 result.pages[0]
306 .systems
307 .iter()
308 .map(|s| s.measure_indices.clone())
309 .collect::<Vec<_>>(),
310 vec![vec![0, 1], vec![2, 3]]
311 );
312 assert_eq!(result.pages[1].systems[0].measure_indices, vec![4]);
313 assert_eq!(result.pages[1].systems[0].page_index, 1);
314 assert_eq!(result.pages[1].systems[0].address.index_on_page, 0);
315 assert_eq!(
316 result.pages[1].systems[0].break_reason,
317 BreakReason::EndOfScore
318 );
319 assert_eq!(result.pages[0].break_reason, BreakReason::PageCapacity);
320 }
321
322 #[test]
323 fn forced_page_break_starts_next_system_on_next_page() {
324 let mut score = score_with_measures(3);
325 score.parts[0].staves[0].measures[0].page_break = true;
326 let result = compute_print_layout(
327 &score,
328 &PrintConfig {
329 measures_per_system: 3,
330 systems_per_page: Some(8),
331 ..PrintConfig::default()
332 },
333 )
334 .expect("valid print config");
335 assert_eq!(result.pages.len(), 2);
336 assert_eq!(result.pages[0].systems[0].measure_indices, vec![0]);
337 assert_eq!(result.pages[1].systems[0].measure_indices, vec![1, 2]);
338 assert_eq!(result.pages[0].break_reason, BreakReason::ExplicitPageBreak);
339 assert_eq!(
340 result.pages[0].systems[0].break_reason,
341 BreakReason::ExplicitPageBreak
342 );
343 }
344
345 #[test]
346 fn rejects_margins_that_leave_no_page_area() {
347 let score = score_with_measures(1);
348 let error = compute_print_layout(
349 &score,
350 &PrintConfig {
351 margin_left_mm: 200.0,
352 ..PrintConfig::default()
353 },
354 )
355 .expect_err("invalid page area");
356 assert_eq!(error, PrintLayoutError::NoUsablePageArea);
357 }
358}