1fn ordered_median(indices: &[usize], values: &[f64]) -> f64 {
5 let n = indices.len();
6 if n % 2 == 1 {
7 values[indices[n / 2]]
8 } else {
9 (values[indices[n / 2 - 1]] + values[indices[n / 2]]) / 2.0
10 }
11}
12
13pub fn ordered_clusters(
15 values: Vec<f64>,
16 tolerance: f64,
17 relative: f64,
18 last_only: bool,
19) -> Option<Vec<Vec<usize>>> {
20 if values.iter().any(|v| !v.is_finite()) || !tolerance.is_finite() || !relative.is_finite() {
21 return None;
22 }
23 let mut order: Vec<_> = (0..values.len()).collect();
24 order.sort_by(|a, b| values[*a].partial_cmp(&values[*b]).unwrap());
25 let mut clusters: Vec<Vec<usize>> = Vec::new();
26 for index in order {
27 let start = if last_only {
28 clusters.len().saturating_sub(1)
29 } else {
30 0
31 };
32 let target = (start..clusters.len()).find(|&i| {
33 let middle = ordered_median(&clusters[i], &values);
34 let limit = if relative == 0.0 {
35 tolerance
36 } else {
37 relative * middle
38 };
39 (values[index] - middle).abs() <= limit
40 });
41 if let Some(target) = target {
42 clusters[target].push(index);
43 } else {
44 clusters.push(vec![index]);
45 }
46 }
47 Some(clusters)
48}
49
50use crate::geometry::Box4;
51use crate::median;
52
53fn median_values<I: IntoIterator<Item = f64>>(values: I) -> f64 {
55 crate::median(values.into_iter().collect())
56}
57
58pub type Typography = (
59 f64,
60 Option<f64>,
61 Option<usize>,
62 f64,
63 Option<f64>,
64 Option<f64>,
65 Option<f64>,
66);
67
68pub fn typography(
70 boxes: Vec<Option<Box4>>,
71 fonts: Vec<Option<usize>>,
72 weights: Vec<Option<f64>>,
73 families: &[Option<usize>],
74 fallback_height: f64,
75) -> Option<Typography> {
76 if boxes.len() != fonts.len()
77 || boxes.len() != weights.len()
78 || fonts.iter().flatten().any(|i| *i >= families.len())
79 || !fallback_height.is_finite()
80 || boxes.iter().flatten().flatten().any(|v| !v.is_finite())
81 || weights.iter().flatten().any(|v| !v.is_finite())
82 {
83 return None;
84 }
85 let glyph_count = boxes.len();
86 let mut heights = Vec::with_capacity(glyph_count);
87 let mut widths = Vec::with_capacity(glyph_count);
88 let mut counts = vec![0usize; families.len()];
89 let mut font_weights = vec![Vec::new(); families.len()];
90 let mut seen = Vec::new();
91 let mut glyphs = Vec::with_capacity(glyph_count);
92 for ((raw, font), weight) in boxes.into_iter().zip(fonts).zip(weights) {
93 let Some(b) = raw else {
94 continue;
95 };
96 heights.push(0.1_f64.max(b[3] - b[1]));
97 widths.push(0.1_f64.max(b[2] - b[0]));
98 if let Some(id) = font {
99 if counts[id] == 0 {
100 seen.push(id);
101 }
102 counts[id] += 1;
103 if let Some(weight) = weight {
104 font_weights[id].push(weight);
105 }
106 }
107 glyphs.push((b, font, weight));
108 }
109 let height = if heights.is_empty() {
110 fallback_height
111 } else {
112 median_values(heights)
113 };
114 let width = if widths.is_empty() {
115 None
116 } else {
117 Some(median_values(widths))
118 };
119 let mut winner = None;
120 for id in seen {
121 if winner.is_none_or(|previous| counts[id] > counts[previous]) {
122 winner = Some(id);
123 }
124 }
125 let coverage = winner.map_or(0.0, |id| {
126 counts[id] as f64 / counts.iter().sum::<usize>() as f64
127 });
128 let weight = winner.and_then(|id| {
129 if font_weights[id].is_empty() {
130 None
131 } else {
132 Some(median_values(font_weights[id].iter().copied()))
133 }
134 });
135 let mut emphasis = None;
136 let mut typography_width = None;
137 if glyphs.len() >= 4 {
138 if let Some(first) = glyphs[0].1 {
139 let split = glyphs
140 .iter()
141 .position(|g| g.1 != Some(first))
142 .unwrap_or(glyphs.len());
143 if split >= 2 && glyphs.len() - split >= 2 {
144 let left = glyphs[..split]
145 .iter()
146 .map(|g| g.0[0])
147 .fold(f64::INFINITY, f64::min);
148 let right = glyphs[..split]
149 .iter()
150 .map(|g| g.0[2])
151 .fold(f64::NEG_INFINITY, f64::max);
152 let prefix_width = 0.1_f64.max(right - left);
153 let prefix: Vec<_> = glyphs[..split].iter().filter_map(|g| g.2).collect();
154 let body: Vec<_> = glyphs[split..].iter().filter_map(|g| g.2).collect();
155 if !prefix.is_empty() && !body.is_empty() {
156 let p = median_values(prefix);
157 let b = median_values(body);
158 if !(p - b < 100.0 || p < 1.15 * 1.0_f64.max(b)) {
160 emphasis = Some(prefix_width);
161 }
162 }
163 if let Some(first_family) = families[first] {
164 let mut body_counts = vec![0; families.len()];
165 let mut body_seen = Vec::new();
166 for (_, id, _) in &glyphs[split..] {
167 if let Some(id) = *id {
168 if body_counts[id] == 0 {
169 body_seen.push(id);
170 }
171 body_counts[id] += 1;
172 }
173 }
174 let mut body_winner = None;
175 for id in body_seen {
176 if body_winner
177 .is_none_or(|previous| body_counts[id] > body_counts[previous])
178 {
179 body_winner = Some(id);
180 }
181 }
182 if let Some(id) = body_winner {
183 if body_counts[id] >= 2 && families[id] != Some(first_family) {
184 typography_width = Some(prefix_width);
185 }
186 }
187 }
188 }
189 }
190 }
191 Some((
192 height,
193 width,
194 winner,
195 coverage,
196 weight,
197 emphasis,
198 typography_width,
199 ))
200}
201
202pub fn lane_gap(
204 boxes: Vec<Box4>,
205 heights: Vec<f64>,
206 restored: Vec<bool>,
207 skip: Vec<bool>,
208) -> Option<(f64, f64)> {
209 let n = boxes.len();
210 if heights.len() != n
211 || restored.len() != n
212 || skip.len() != n.saturating_sub(1)
213 || boxes.iter().flatten().any(|v| !v.is_finite())
214 || heights.iter().any(|v| !v.is_finite() || *v <= 0.0)
215 {
216 return None;
217 }
218 let mh = if n == 0 { 1.0 } else { median(heights.clone()) };
219 if !mh.is_finite() {
220 return None;
221 }
222 let mut gaps = Vec::new();
223 for i in 1..n {
224 if skip[i - 1] {
225 continue;
226 }
227 let a = boxes[i - 1];
228 let b = boxes[i];
229 let ah = heights[i - 1];
230 let bh = heights[i];
231 if ah.max(bh) / ah.min(bh) > 1.35 {
232 continue;
233 }
234 let h = ah.max(bh);
235 let gap = if restored[i - 1] {
236 (b[1] - a[3]).max(-0.25 * ah)
237 } else {
238 b[1] - (a[1] + ah)
239 };
240 if gap < -0.25 * h || gap > 2.0 * h {
241 continue;
242 }
243 let overlap = 0.0_f64.max(a[2].min(b[2]) - a[0].max(b[0]));
244 let shorter = (a[2] - a[0]).min(b[2] - b[0]);
245 let ratio = if shorter > 0.0 {
246 overlap / shorter
247 } else {
248 0.0
249 };
250 if ratio < 0.5 && (a[0] - b[0]).abs() > 1.5 * mh {
251 continue;
252 }
253 if !gap.is_finite() {
254 return None;
255 }
256 gaps.push(0.0_f64.max(gap));
257 }
258 if gaps.is_empty() {
259 return Some((0.35 * mh, 0.0));
260 }
261 gaps.sort_by(|a, b| a.partial_cmp(b).unwrap());
262 let count = 1usize.max((gaps.len() as f64 * 0.6).ceil() as usize);
263 let lower = gaps[..count].to_vec();
264 let regular = median(lower.clone());
265 if !regular.is_finite() {
266 return None;
267 }
268 let mad = median(lower.into_iter().map(|g| (g - regular).abs()).collect());
269 if !mad.is_finite() {
270 return None;
271 }
272 Some((regular, mad))
273}
274pub struct NoteMetrics {
276 items: Vec<(i64, f64, f64)>,
277 extents: std::collections::HashMap<i64, (f64, f64)>,
278 index: crate::note_index::HeightIndex,
279}
280
281impl NoteMetrics {
282 pub fn new(mut items: Vec<(i64, f64, f64)>) -> Option<Self> {
284 if items
285 .iter()
286 .any(|(_, y, h)| !y.is_finite() || !h.is_finite())
287 {
288 return None;
289 }
290 let mut extents = std::collections::HashMap::<i64, (f64, f64)>::new();
291 for &(id, y, _) in &items {
292 extents
293 .entry(id)
294 .and_modify(|p| {
295 p.0 = p.0.min(y);
296 p.1 = p.1.max(y);
297 })
298 .or_insert((y, y));
299 }
300 let index = crate::note_index::HeightIndex::new(&items);
301 items.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
302 Some(Self {
303 items,
304 extents,
305 index,
306 })
307 }
308
309 pub fn rows(&self, members: Vec<Vec<i64>>) -> crate::note_index::CoreRows {
311 crate::note_index::CoreRows::new(members, &self.extents)
312 }
313
314 pub fn height_for_rows(
316 &self,
317 rows: &crate::note_index::CoreRows,
318 start: usize,
319 end: usize,
320 top: f64,
321 bottom: f64,
322 fallback: f64,
323 ) -> Option<f64> {
324 if start > end || end > rows.members.len() {
325 return None;
326 }
327 if rows.contained(start, end, top, bottom) {
328 return self.index.height(top, bottom, fallback);
329 }
330 let core: Vec<i64> = rows.members[start..end].iter().flatten().copied().collect();
331 self.height(top, bottom, &core, fallback)
332 }
333
334 pub fn height(&self, top: f64, bottom: f64, core: &[i64], fallback: f64) -> Option<f64> {
336 if !top.is_finite() || !bottom.is_finite() || !fallback.is_finite() {
337 return None;
338 }
339 let excluded: std::collections::HashSet<i64> = core
341 .iter()
342 .copied()
343 .filter(|id| {
344 self.extents
345 .get(id)
346 .is_some_and(|(a, b)| *a < top || *b > bottom)
347 })
348 .collect();
349 let heights: Vec<f64> = self
350 .items
351 .iter()
352 .filter_map(|(id, y, h)| {
353 ((*y < top || *y > bottom) && !excluded.contains(id)).then_some(*h)
354 })
355 .collect();
356 let n = heights.len();
357 if n < 4 {
358 return Some(fallback);
359 }
360 let count = n.div_ceil(4);
361 let start = n - count;
362 let value = if count % 2 == 1 {
363 heights[start + count / 2]
364 } else {
365 (heights[start + count / 2 - 1] + heights[start + count / 2]) / 2.0
366 };
367 value.is_finite().then_some(value)
368 }
369}