Skip to main content

docvortex_core/
statistics.rs

1//! 对单调追加的聚类直接读取中位位置,保持原比较顺序和成员索引。
2
3/// 输入已稳定排序;每个簇按全局顺序追加,不需要再次排序。
4fn 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
13/// 按绝对或相对容差选首个簇,last_only 保留只检查末簇的原策略。
14pub 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
53/// 按调用方提供的值计算中位数,避免为了切片临时克隆整份向量。
54fn 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
68/// 一次计算整行字体与字形统计;类别编号由 Python 按其字符串/整数语义提供。
69pub 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                    // 中位数相加可能溢出,保留 Python 两个小于判断的否定,不能改写为大于等于。
159                    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
202/// 输入沿 Python 的稳定成员排序;快照只在本次调用使用,不缓存可变行。
203pub 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}
274/// 只在一次表格候选构建期间保存排序后的正文高度与来源中心范围。
275pub 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    /// 将高度稳定排序一次,后续区间过滤保持 Python 排序后的平局顺序。
283    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    /// 建立来源中心的行范围索引,整个走廊只传输一次成员。
310    pub fn rows(&self, members: Vec<Vec<i64>>) -> crate::note_index::CoreRows {
311        crate::note_index::CoreRows::new(members, &self.extents)
312    }
313
314    /// 已证明核心全在排除区间时查询秩树,否则保留原生精确成员筛选。
315    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    /// 过滤纵向排除区间和明确核心成员,直接读取最高四分位的中位位置。
335    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        // 处于排除区间内部的成员无需再次建哈希集合,重复来源仍按其全部中心范围检查。
340        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}