File size: 2,375 Bytes
78738de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""Extract quỹ đạo polyline từ System sau simulate(render=True) — MỚI 23/07/2026.

Đúng logic render_png CLI đang dùng: ưu tiên history_cts (đã continuize
dt=0.01), fallback history event-based; vectorize() → rvw[:, 0, :2].
Downsample còn ≤ max_points điểm/bi để JSON nhẹ (~10 KB/cú).

25/07/2026 (Production §2e): thêm ``min_disp`` OPT-IN để lọc bi đứng yên.
``continuize`` điền history cho MỌI bi kể cả bi không hề nhúc nhích → bàn
full rack cho ra 200 điểm × 10 bi × 4 cú ≈ 250-350 KB JSON/response (đo bàn
3 bi chỉ ~10 KB/cú). Mặc định None = hành vi cũ y nguyên (đường v1 + CLI
render_png KHÔNG được đổi một pixel).
"""

from __future__ import annotations


def extract_trajectories(system, max_points: int = 200,
                         min_disp: float | None = None) -> dict:
    """{ball_id: [[x, y], ...]} — chỉ gồm bi có quỹ đạo ≥ 2 điểm.

    min_disp: bỏ qua bi có ĐỘ DÀI ĐƯỜNG ĐI < min_disp (mét). Cố ý dùng độ
    dài đường đi chứ không phải dịch chuyển ròng đầu→cuối: bi bị đánh đi rồi
    dội băng về gần chỗ cũ có dịch chuyển ròng ~0 nhưng đó chính là quỹ đạo
    người dùng cần thấy. None (mặc định) = không lọc.
    """
    import numpy as np

    out: dict = {}
    if system is None:
        return out
    for bid, ball in system.balls.items():
        xy = None
        for hist in (getattr(ball, "history_cts", None),
                     getattr(ball, "history", None)):
            if hist is None:
                continue
            try:
                rvw, _ss, _ts = hist.vectorize()
            except Exception:
                continue
            if rvw is not None and len(rvw) > 1:
                xy = rvw[:, 0, :2]
                break
        if xy is None:
            continue
        if min_disp is not None:
            # đo trên mảng ĐẦY ĐỦ, trước khi downsample
            path = float(np.linalg.norm(np.diff(xy, axis=0), axis=1).sum())
            if path < min_disp:
                continue
        if len(xy) > max_points:
            idx = np.round(np.linspace(0, len(xy) - 1, max_points)).astype(int)
            xy = xy[idx]
        out[bid] = [[float(p[0]), float(p[1])] for p in xy]
    return out