Skip to main content

docvortex_core/
tables.rs

1//! 批量计算表格筛选、线段覆盖、网格连通和字符归属;候选裁决留在 Python。
2
3use crate::geometry::{self, Box4};
4use std::collections::HashMap;
5pub type Rule = (u8, f64, f64, f64);
6pub type Query = (u8, Vec<f64>, f64, f64, f64);
7pub type GridIndex = (Vec<f64>, Vec<f64>, Vec<Vec<usize>>);
8type CoverageCache = HashMap<(u8, Vec<u64>, u64), Vec<(f64, f64)>>;
9pub type ComponentSpecs = (Vec<usize>, Option<Vec<(usize, usize, usize, usize)>>);
10
11/// 取得矩形交集,边界相接仍视为无面积。
12fn intersection(a: Box4, b: Box4) -> Option<Box4> {
13    let c = [
14        a[0].max(b[0]),
15        a[1].max(b[1]),
16        a[2].min(b[2]),
17        a[3].min(b[3]),
18    ];
19    if c[2] > c[0] && c[3] > c[1] {
20        Some(c)
21    } else {
22        None
23    }
24}
25
26/// 批量筛出中心落在表格内的源框,并预先计算局部坐标。
27pub fn select_boxes(
28    values: Vec<Option<Box4>>,
29    table: Box4,
30    angle: i32,
31) -> Vec<(usize, Box4, Option<Box4>)> {
32    let mut result = Vec::new();
33    for (i, b) in values.into_iter().enumerate() {
34        let Some(b) = geometry::normalize(b, false) else {
35            continue;
36        };
37        let x = (b[0] + b[2]) / 2.0;
38        let y = (b[1] + b[3]) / 2.0;
39        if !(table[0] <= x && x <= table[2] && table[1] <= y && y <= table[3]) {
40            continue;
41        }
42        let local = intersection(b, table).map(|b| {
43            geometry::rotate(
44                [
45                    b[0] - table[0],
46                    b[1] - table[1],
47                    b[2] - table[0],
48                    b[3] - table[1],
49                ],
50                [table[2] - table[0], table[3] - table[1]],
51                angle,
52            )
53        });
54        result.push((i, b, local));
55    }
56    result
57}
58
59/// 用稳定端点排序与顺序累加计算覆盖率。
60fn coverage(intervals: &[(f64, f64)], start: f64, end: f64) -> f64 {
61    if end <= start {
62        return 0.0;
63    }
64    let mut clipped: Vec<_> = intervals
65        .iter()
66        .filter_map(|(a, b)| {
67            let left = start.max(a.min(*b));
68            let right = end.min(a.max(*b));
69            if right > left {
70                Some((left, right))
71            } else {
72                None
73            }
74        })
75        .collect();
76    clipped.sort_by(|a, b| a.partial_cmp(b).unwrap());
77    let Some(&(mut left, mut right)) = clipped.first() else {
78        return 0.0;
79    };
80    let mut covered = 0.0;
81    for &(a, b) in &clipped[1..] {
82        if a <= right {
83            right = right.max(b);
84        } else {
85            covered += right - left;
86            left = a;
87            right = b;
88        }
89    }
90    covered += right - left;
91    1.0_f64.min(covered / (end - start))
92}
93
94/// 在一次调用中复用轨道区间,避免每个 separator 单独跨语言调用。
95pub fn coverage_batch(rules: Vec<Rule>, queries: Vec<Query>) -> Option<Vec<f64>> {
96    if rules
97        .iter()
98        .any(|r| ![r.1, r.2, r.3].iter().all(|v| v.is_finite()))
99        || queries.iter().any(|q| {
100            q.1.iter()
101                .chain([q.2, q.3, q.4].iter())
102                .any(|v| !v.is_finite())
103        })
104    {
105        return None;
106    }
107    let mut cache = CoverageCache::new();
108    Some(
109        queries
110            .into_iter()
111            .map(|(orientation, aliases, start, end, tolerance)| {
112                let key = (
113                    orientation,
114                    aliases.iter().map(|v| v.to_bits()).collect(),
115                    tolerance.to_bits(),
116                );
117                let intervals = cache.entry(key).or_insert_with(|| {
118                    rules
119                        .iter()
120                        .filter(|r| {
121                            r.0 == orientation
122                                && aliases.iter().any(|a| (r.1 - *a).abs() <= tolerance)
123                        })
124                        .map(|r| (r.2, r.3))
125                        .collect()
126                });
127                coverage(intervals, start, end)
128            })
129            .collect(),
130    )
131}
132
133/// 使用 Python 已计算的簇均值合并整批线段,保持求和/舍入的版本语义。
134pub fn merge_rules(
135    rules: Vec<Rule>,
136    coordinates: Vec<(u8, Vec<f64>)>,
137    tolerance: f64,
138    join: f64,
139) -> Option<Vec<Rule>> {
140    if rules
141        .iter()
142        .any(|r| ![r.1, r.2, r.3].iter().all(|v| v.is_finite()))
143        || coordinates
144            .iter()
145            .flat_map(|r| &r.1)
146            .any(|v| !v.is_finite())
147    {
148        return None;
149    }
150    let mut output = Vec::new();
151    for (orientation, coords) in coordinates {
152        for coordinate in coords {
153            let mut intervals: Vec<_> = rules
154                .iter()
155                .filter(|r| r.0 == orientation && (r.1 - coordinate).abs() <= tolerance)
156                .map(|r| (r.2.min(r.3), r.2.max(r.3)))
157                .collect();
158            intervals.sort_by(|a, b| a.partial_cmp(b).unwrap());
159            let Some(&(mut start, mut end)) = intervals.first() else {
160                continue;
161            };
162            for &(a, b) in &intervals[1..] {
163                if a <= end + join {
164                    end = end.max(b);
165                } else {
166                    output.push((orientation, coordinate, start, end));
167                    start = a;
168                    end = b;
169                }
170            }
171            output.push((orientation, coordinate, start, end));
172        }
173    }
174    Some(output)
175}
176
177/// 计算字符相交面积比例,维持原来的上界截断。
178fn overlap(glyph: Box4, cell: Box4) -> f64 {
179    let area = (glyph[2] - glyph[0]) * (glyph[3] - glyph[1]);
180    let Some(b) = intersection(glyph, cell) else {
181        return 0.0;
182    };
183    if area <= 0.0 {
184        0.0
185    } else {
186        1.0_f64.min((b[2] - b[0]) * (b[3] - b[1]) / area)
187    }
188}
189
190/// 检查中心归属,供零交叠情况下的原规则回退使用。
191fn contains(cell: Box4, glyph: Box4) -> bool {
192    let x = (glyph[0] + glyph[2]) / 2.0;
193    let y = (glyph[1] + glyph[3]) / 2.0;
194    cell[0] <= x && x <= cell[2] && cell[1] <= y && y <= cell[3]
195}
196
197/// 将二分位置限制在实际原子轨道范围内。
198fn track_index(tracks: &[f64], value: f64, right: bool) -> usize {
199    let at = if right {
200        tracks.partition_point(|v| *v <= value)
201    } else {
202        tracks.partition_point(|v| *v < value)
203    };
204    at.saturating_sub(1).min(tracks.len() - 2)
205}
206
207/// 整张表格执行现有索引或穷举落格判定,相同比例仍优先最大单元格索引。
208pub fn assign_cells(
209    glyphs: Vec<Box4>,
210    specs: Vec<Box4>,
211    index: Option<GridIndex>,
212) -> Option<Vec<(Option<usize>, bool)>> {
213    if specs.is_empty()
214        || glyphs
215            .iter()
216            .chain(&specs)
217            .flatten()
218            .any(|v| !v.is_finite())
219    {
220        return None;
221    }
222    if let Some((x, y, owners)) = &index {
223        if x.len() < 2
224            || y.len() < 2
225            || x.iter().chain(y).any(|v| !v.is_finite())
226            || owners.len() != y.len() - 1
227            || owners
228                .iter()
229                .any(|row| row.len() != x.len() - 1 || row.iter().any(|i| *i >= specs.len()))
230        {
231            return None;
232        }
233    }
234    Some(
235        glyphs
236            .into_iter()
237            .map(|glyph| {
238                let candidates = if let Some((x, y, owners)) = &index {
239                    let left = track_index(x, glyph[0], true);
240                    let right = track_index(x, glyph[2], false);
241                    let top = track_index(y, glyph[1], true);
242                    let bottom = track_index(y, glyph[3], false);
243                    if left == right && top == bottom {
244                        let i = owners[top][left];
245                        return (
246                            if overlap(glyph, specs[i]) > 0.0 || contains(specs[i], glyph) {
247                                Some(i)
248                            } else {
249                                None
250                            },
251                            false,
252                        );
253                    }
254                    let mut indices: Vec<_> = (top..=bottom)
255                        .flat_map(|r| (left..=right).map(move |c| owners[r][c]))
256                        .collect();
257                    indices.sort_unstable();
258                    indices.dedup();
259                    indices
260                } else {
261                    (0..specs.len()).collect()
262                };
263                let mut overlaps: Vec<_> = candidates
264                    .iter()
265                    .map(|i| (overlap(glyph, specs[*i]), *i))
266                    .collect();
267                overlaps.sort_by(|a, b| b.partial_cmp(a).unwrap());
268                let Some(&(ratio, i)) = overlaps.first() else {
269                    return (None, false);
270                };
271                if ratio <= 0.0 {
272                    let containing: Vec<_> = candidates
273                        .into_iter()
274                        .filter(|j| contains(specs[*j], glyph))
275                        .collect();
276                    return (
277                        if containing.len() == 1 {
278                            Some(containing[0])
279                        } else {
280                            None
281                        },
282                        false,
283                    );
284                }
285                (
286                    Some(i),
287                    overlaps.len() > 1
288                        && overlaps[1].0 >= 0.45
289                        && (ratio - overlaps[1].0).abs() <= 0.02,
290                )
291            })
292            .collect(),
293    )
294}
295
296/// 对既有父节点执行完整路径压缩,与表格并查集的递归 find 等价。
297fn find(parents: &mut [usize], index: usize) -> usize {
298    let mut root = index;
299    while parents[root] != root {
300        root = parents[root];
301    }
302    let mut i = index;
303    while parents[i] != i {
304        let next = parents[i];
305        parents[i] = root;
306        i = next;
307    }
308    root
309}
310
311/// 批量连接原子格,并保留 first-root 拥有 second-root 的规则。
312pub fn grid_parents(count: usize, pairs: Vec<(usize, usize)>) -> Vec<usize> {
313    let mut parents: Vec<_> = (0..count).collect();
314    for (a, b) in pairs {
315        let a = find(&mut parents, a);
316        let b = find(&mut parents, b);
317        if a != b {
318            parents[b] = a;
319        }
320    }
321    parents
322}
323
324/// 将连通格转换为矩形范围;不完整矩形仍拒绝整份候选。
325pub fn component_specs(mut parents: Vec<usize>, rows: usize, cols: usize) -> ComponentSpecs {
326    let mut groups: HashMap<usize, (usize, usize, usize, usize, usize)> = HashMap::new();
327    for r in 0..rows {
328        for c in 0..cols {
329            let root = find(&mut parents, r * cols + c);
330            let g = groups.entry(root).or_insert((r, c, r, c, 0));
331            g.0 = g.0.min(r);
332            g.1 = g.1.min(c);
333            g.2 = g.2.max(r);
334            g.3 = g.3.max(c);
335            g.4 += 1;
336        }
337    }
338    let mut specs = Vec::new();
339    for (r, c, last_r, last_c, n) in groups.into_values() {
340        if (last_r - r + 1) * (last_c - c + 1) != n {
341            return (parents, None);
342        }
343        specs.push((r, c, last_r - r + 1, last_c - c + 1));
344    }
345    specs.sort_unstable();
346    (parents, Some(specs))
347}
348
349/// 保持反向候选遍历、严格距离平局与前缀下界提前停止的视觉组行。
350pub fn visual_rows(
351    boxes: Vec<Box4>,
352    ids: Vec<usize>,
353    median_height: f64,
354) -> Option<Vec<Vec<usize>>> {
355    if boxes.len() != ids.len()
356        || !median_height.is_finite()
357        || boxes
358            .iter()
359            .flatten()
360            .any(|v| !v.is_finite() || v.abs() > 1e150)
361    {
362        return None;
363    }
364    let mut order: Vec<_> = (0..boxes.len()).collect();
365    order.sort_by(|&a, &b| {
366        let aa = boxes[a];
367        let bb = boxes[b];
368        ((aa[1] + aa[3]) / 2.0)
369            .partial_cmp(&((bb[1] + bb[3]) / 2.0))
370            .unwrap()
371            .then_with(|| aa[0].partial_cmp(&bb[0]).unwrap())
372            .then_with(|| ids[a].cmp(&ids[b]))
373    });
374    let mut rows: Vec<Vec<usize>> = Vec::new();
375    let mut bounds: Vec<Box4> = Vec::new();
376    let mut prefix: Vec<f64> = Vec::new();
377    let tolerance = 0.75_f64.max(median_height * 0.40);
378    for index in order {
379        let g = boxes[index];
380        let cy = (g[1] + g[3]) / 2.0;
381        let mut best = None;
382        let mut best_distance = f64::INFINITY;
383        for i in (0..bounds.len()).rev() {
384            let r = bounds[i];
385            let ry = (r[1] + r[3]) / 2.0;
386            let distance = (cy - ry).abs();
387            let overlap = g[3].min(r[3]) - g[1].max(r[1]);
388            let height = (g[3] - g[1]).min(r[3] - r[1]);
389            let ratio = if overlap <= 0.0 || height <= 0.0 {
390                0.0
391            } else {
392                1.0_f64.min(overlap / height)
393            };
394            if (ratio >= 0.45 || distance <= tolerance) && distance < best_distance {
395                best = Some(i);
396                best_distance = distance;
397            }
398            if cy >= ry && cy - ry > tolerance && g[1] > prefix[i] {
399                break;
400            }
401        }
402        if let Some(i) = best {
403            rows[i].push(index);
404            let r = bounds[i];
405            bounds[i] = [
406                r[0].min(g[0]),
407                r[1].min(g[1]),
408                r[2].max(g[2]),
409                r[3].max(g[3]),
410            ];
411            for j in i..bounds.len() {
412                prefix[j] = if j == 0 {
413                    bounds[j][3]
414                } else {
415                    prefix[j - 1].max(bounds[j][3])
416                };
417            }
418        } else {
419            rows.push(vec![index]);
420            bounds.push(g);
421            prefix.push(prefix.last().copied().unwrap_or(g[3]).max(g[3]));
422        }
423    }
424    let mut row_order: Vec<_> = (0..rows.len()).collect();
425    row_order.sort_by(|&a, &b| {
426        bounds[a][1]
427            .partial_cmp(&bounds[b][1])
428            .unwrap()
429            .then_with(|| bounds[a][0].partial_cmp(&bounds[b][0]).unwrap())
430    });
431    Some(
432        row_order
433            .into_iter()
434            .map(|i| {
435                let mut row = std::mem::take(&mut rows[i]);
436                row.sort_by(|&a, &b| {
437                    boxes[a][0]
438                        .partial_cmp(&boxes[b][0])
439                        .unwrap()
440                        .then_with(|| boxes[a][1].partial_cmp(&boxes[b][1]).unwrap())
441                        .then_with(|| ids[a].cmp(&ids[b]))
442                });
443                row
444            })
445            .collect(),
446    )
447}
448
449/// 整表计算列占用;公共边界仍归属从左到右第一个符合区间的列。
450pub fn row_occupancy(rows: Vec<Vec<f64>>, tracks: Vec<f64>) -> Option<Vec<Vec<usize>>> {
451    if rows
452        .iter()
453        .flatten()
454        .chain(tracks.iter())
455        .any(|v| !v.is_finite())
456    {
457        return None;
458    }
459    Some(
460        rows.into_iter()
461            .map(|row| {
462                let mut occupied = vec![false; tracks.len().saturating_sub(1)];
463                for center in row {
464                    if let Some(i) = tracks
465                        .windows(2)
466                        .position(|t| t[0] <= center && center <= t[1])
467                    {
468                        occupied[i] = true;
469                    }
470                }
471                occupied
472                    .into_iter()
473                    .enumerate()
474                    .filter_map(|(i, yes)| yes.then_some(i))
475                    .collect()
476            })
477            .collect(),
478    )
479}
480
481/// 单元格一次完成原视觉组的几何细分,只返回完整源索引,不缓存文字或认领结果。
482pub fn cell_visual_groups(
483    boxes: Vec<Box4>,
484    ids: Vec<usize>,
485    visual: Vec<i64>,
486    median_height: f64,
487) -> Option<Vec<Vec<usize>>> {
488    if boxes.len() != ids.len()
489        || boxes.len() != visual.len()
490        || !median_height.is_finite()
491        || boxes
492            .iter()
493            .flatten()
494            .any(|v| !v.is_finite() || v.abs() > 1e100)
495    {
496        return None;
497    }
498    let mut groups = std::collections::BTreeMap::<i64, Vec<usize>>::new();
499    for (index, row) in visual.into_iter().enumerate() {
500        groups.entry(row).or_default().push(index);
501    }
502    let mut refined = Vec::new();
503    for original in groups.into_values() {
504        // 两个合格细分行各至少含两个字形,少于四个时只能保留原组。
505        if original.len() < 4 {
506            refined.push(original);
507            continue;
508        }
509        let local = visual_rows(
510            original.iter().map(|i| boxes[*i]).collect(),
511            original.iter().map(|i| ids[*i]).collect(),
512            median_height,
513        )?;
514        let coherent = local.iter().all(|row| {
515            let centers: Vec<f64> = row
516                .iter()
517                .map(|i| {
518                    let b = boxes[original[*i]];
519                    (b[1] + b[3]) / 2.0
520                })
521                .collect();
522            let low = centers.iter().copied().reduce(f64::min).unwrap();
523            let high = centers.iter().copied().reduce(f64::max).unwrap();
524            high - low <= 0.3 * median_height
525        });
526        if local.len() > 1
527            && coherent
528            && local.iter().all(|row| {
529                let height = crate::median(
530                    row.iter()
531                        .map(|i| {
532                            let b = boxes[original[*i]];
533                            b[3] - b[1]
534                        })
535                        .collect(),
536                );
537                row.len() >= 2 && 0.85 * median_height <= height && height <= 1.25 * median_height
538            })
539        {
540            refined.extend(
541                local
542                    .into_iter()
543                    .map(|row| row.into_iter().map(|i| original[i]).collect()),
544            );
545        } else {
546            refined.push(original);
547        }
548    }
549    Some(refined)
550}