Skip to main content

atmos/os_lib/web_engine/
paint_cull.rs

1//! 要素がフレームのどこへ描かれるか、そして描かれるかどうかの判定(純粋関数)。
2//!
3//! ## なぜ切り出すか
4//! 「この要素は画面に出るはずなのに出ない」を調べるのに、
5//! ビルド(約 10 分)と QEMU 実行(約 6 分)を何度も回していた。
6//! **座標変換と可視判定は入力と出力が決まっている純粋な計算**で、
7//! 本来は単体試験で答えが出る。実行して確かめる対象ではない。
8//!
9//! ここに切り出しておけば、「y_offset=1627 の要素は scroll_y=1269・
10//! top_offset=40・win_h=549 のとき描かれるか」に**ビルドせずに答えられる**。
11//!
12//! ## 座標系
13//! - `y_offset`: ドキュメント先頭からの位置(レイアウトが決める)
14//! - `el_y`: ビューポート内の位置 = `y_offset - scroll_y + top_offset`
15//! - `draw_y`: 描画先バッファ内の位置 = `win_y + el_y`
16//!
17//! `top_offset` はブラウザのツールバー等の高さで、
18//! コンテンツはその下から始まる。
19
20/// ドキュメント座標をビューポート座標へ変換する。
21///
22/// 計算量: **O(1)**。
23pub fn viewport_y(y_offset: i32, scroll_y: i32, top_offset: i32) -> i32 {
24    y_offset - scroll_y + top_offset
25}
26
27/// ビューポート座標を描画先バッファ座標へ変換する。
28///
29/// 計算量: **O(1)**。
30pub fn buffer_y(el_y: i32, win_y: i32) -> i32 {
31    win_y + el_y
32}
33
34/// 要素がビューポートに掛かっているか(描画対象か)。
35///
36/// 計算量: **O(1)**。
37///
38/// `render.rs` の要素ループが使っている判定と同じ:
39/// 上端がツールバーより上で完全に隠れている、または
40/// 下端がビューポート下端より下なら描かない。
41pub fn is_in_viewport(el_y: i32, height: i32, top_offset: i32, win_h: i32) -> bool {
42    !(el_y + height < top_offset || el_y > win_h)
43}
44
45/// 描画先バッファのクリップ矩形に掛かっているか。
46///
47/// 計算量: **O(1)**。
48///
49/// ウィンドウマネージャは `set_clip(app_x, app_y, w, h)` で
50/// アプリ領域だけを描けるようにしている。`Screen::draw_pixel` は
51/// この矩形の外を捨てるため、ここを外れた要素は**何も出ない**。
52pub fn is_in_clip(
53    draw_x: i32,
54    draw_y: i32,
55    width: i32,
56    height: i32,
57    clip: (i32, i32, i32, i32),
58) -> bool {
59    let (cx0, cy0, cx1, cy1) = clip;
60    !(draw_x + width <= cx0 || draw_x >= cx1 || draw_y + height <= cy0 || draw_y >= cy1)
61}
62
63/// `transform: scale()` を反映した実効サイズ。
64///
65/// 計算量: **O(1)**。
66///
67/// `scale` が 1 に十分近ければ元の値をそのまま返す(浮動小数の丸めを避ける)。
68///
69/// **注意**: `scale` が 0 だと実効サイズが 0 になり、
70/// その要素の矩形は潰れる。CSS アニメーションの初期状態が
71/// `transform: scale(0)` の要素は、アニメーションが進むまで
72/// 何も描かれない(実ブラウザと同じ挙動)。
73pub fn effective_size(base: i32, scale: f32) -> i32 {
74    if (scale - 1.0f32).abs() < 0.001 {
75        base
76    } else {
77        (base as f32 * scale) as i32
78    }
79}
80
81/// 実効サイズが 0 以下で、塗っても 1 ピクセルも出ない状態か。
82///
83/// 計算量: **O(1)**。
84pub fn is_degenerate(eff_w: i32, eff_h: i32) -> bool {
85    eff_w <= 0 || eff_h <= 0
86}
87
88/// `render.rs` が実際に行っている描画座標の算出を、そのまま写したもの。
89///
90/// ```text
91/// let draw_y = win_y + el_y as u32;
92/// ```
93///
94/// 計算量: **O(1)**。
95///
96/// **危険**: `el_y` が負のとき `as u32` で巨大な値へ折り返す。
97/// 画面より上へスクロールアウトした要素は `el_y` が負になるため、
98/// この経路を通る値は常に折り返しの可能性がある。
99/// 折り返した結果は `boxfill` の `y0 > y1` になって描画されないだけなので
100/// 実害は出にくいが、**`y1` 側だけ折り返すと巨大な矩形を塗る**ため危険。
101pub fn draw_y_as_impl(win_y: u32, el_y: i32) -> u32 {
102    win_y.wrapping_add(el_y as u32)
103}
104
105/// 矩形の下端。`render.rs` の `y1_rect = (draw_y + eff_h as u32) as i32` と同じ。
106///
107/// 計算量: **O(1)**。
108pub fn rect_bottom_as_impl(draw_y: u32, eff_h: i32) -> i32 {
109    draw_y.wrapping_add(eff_h as u32) as i32
110}
111
112/// `transform-origin` による描画位置の補正。
113///
114/// 計算量: **O(1)**。
115///
116/// 【2026-08-04 発見・修正】ここは以前 `.max(0)` で 0 にクランプしていた。
117/// `draw_y` は画面より上へスクロールアウトした要素で u32 へ折り返しており
118/// (`draw_y as i32` が負)、そこへクランプを掛けると **0 に固定**される。
119/// 高さはそのままなので `y=0..height` を塗り、**可視領域を丸ごと覆う**。
120///
121/// 実測: 要素(`y_offset=529 h=740`、スクロール後 `el_y=-700`)が
122/// `boxfill(100, 0, 1024, 740)` を実行して研究カードを消していた。
123///
124/// 補正が要るのは `transform: scale()` で寸法が変わったときだけ。
125/// 補正量が 0 なら元の値をそのまま返す。
126pub fn apply_origin_adjust(draw_y: u32, height: i32, eff_h: i32, origin_y: f32) -> u32 {
127    let adj = ((height - eff_h) as f32 * origin_y) as i32;
128    if adj == 0 {
129        draw_y
130    } else {
131        (draw_y as i32).wrapping_add(adj) as u32
132    }
133}