wintf 0.0.3

Windows Tategaki Framework - Rust UI library with Japanese vertical text support
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
//! αマスクデータ構造
//!
//! ビットパック形式(1ビット/ピクセル)でαマスクを保持し、
//! 高速なヒット判定を提供する。

/// 固定閾値(α ≧ 128 でヒット対象)
const ALPHA_THRESHOLD: u8 = 128;

/// αマスクデータ構造
///
/// ビットパック形式でマスクデータを保持:
/// - 1ビット/ピクセル(8ピクセル/バイト)
/// - MSBファースト(ビット7 = 最左ピクセル)
/// - 行ごとに8ピクセル単位でアラインメント
// `PartialEq` は再生成(`regenerate_from_pbgra32`)と新規生成(`from_pbgra32`)の等価檻の
// 比較器として derive する(data/width/height の全一致=マスクの完全同一)。公開挙動への
// 影響はない(追加の trait 実装のみ)。
#[derive(Debug, Clone, PartialEq)]
pub struct AlphaMask {
    data: Vec<u8>,
    width: u32,
    height: u32,
}

/// PBGRA32 の α をビットパック済みバッファへ書き込む(共有本体)
///
/// `from_pbgra32`(新規確保)と `regenerate_from_pbgra32`(バッファ再利用)の**唯一の
/// 実装**。両者の差はバッファの用意の仕方だけであり、ビットパックの意味論はここに一本化
/// されている(二重実装による乖離を構造で防ぐ)。
///
/// # 前提
/// `data` は `row_bytes * height` の長さで、**全バイトが 0 に初期化済み**であること
/// (本関数はセットするビットしか書かないため、残留ビットは消さない)。
fn pack_pbgra32_alpha(
    data: &mut [u8],
    pixels: &[u8],
    width: u32,
    height: u32,
    stride: u32,
    row_bytes: usize,
) {
    for y in 0..height {
        let row_start = (y * stride) as usize;
        for x in 0..width {
            // PBGRA32: 4バイト/ピクセル、α値は4バイト目(オフセット+3)
            let pixel_offset = row_start + (x as usize * 4);
            let alpha = pixels.get(pixel_offset + 3).copied().unwrap_or(0);

            // 閾値128で2値化
            if alpha >= ALPHA_THRESHOLD {
                // MSBファースト: ビット7 = 最左ピクセル
                let byte_index = (y as usize * row_bytes) + (x as usize / 8);
                let bit_index = 7 - (x % 8);
                data[byte_index] |= 1 << bit_index;
            }
        }
    }
}

impl AlphaMask {
    /// PBGRA32ピクセルデータからαマスクを生成
    ///
    /// # Arguments
    /// - `pixels`: PBGRA32形式のピクセルデータ(B, G, R, A の順)
    /// - `width`: 画像幅(ピクセル)
    /// - `height`: 画像高さ(ピクセル)
    /// - `stride`: 行あたりのバイト数
    ///
    /// # Returns
    /// 生成されたαマスク
    pub fn from_pbgra32(pixels: &[u8], width: u32, height: u32, stride: u32) -> Self {
        // 行あたりのバイト数(8ピクセル単位でアラインメント)
        let row_bytes = width.div_ceil(8) as usize;
        let mut data = vec![0u8; row_bytes * height as usize];

        pack_pbgra32_alpha(&mut data, pixels, width, height, stride, row_bytes);

        Self {
            data,
            width,
            height,
        }
    }

    /// 既存バッファを再利用してαマスクの内容を再生成する
    ///
    /// [`from_pbgra32`](Self::from_pbgra32) と**同一入力に対して同一結果**を作るが、
    /// 新しいバッファを確保せず `self.data` を `clear` + `resize` で作り直す
    /// (容量は保持されるため、同寸または縮小方向の再生成では確保が発生しない)。
    /// 定常状態の毎コマ適用で当たり判定マスクの新規確保を無くすための口(要件 3.1)。
    ///
    /// `clear` してから 0 で `resize` するため、**寸法が縮む方向でも前の内容は 1 ビットも
    /// 残らない**(`resize` 単独だと保持された前半バイトのビットが生き残る)。
    ///
    /// # Arguments
    /// - `pixels`: PBGRA32形式のピクセルデータ(B, G, R, A の順)
    /// - `width`: 画像幅(ピクセル)
    /// - `height`: 画像高さ(ピクセル)
    /// - `stride`: 行あたりのバイト数
    pub fn regenerate_from_pbgra32(&mut self, pixels: &[u8], width: u32, height: u32, stride: u32) {
        // 行あたりのバイト数(8ピクセル単位でアラインメント)
        let row_bytes = width.div_ceil(8) as usize;
        let len = row_bytes * height as usize;

        // clear → resize(len, 0) で「全バイト 0・長さ len」を作る。容量は保持されるので
        // len <= capacity の間は確保が起きない。clear を挟むことで縮小方向でも残留しない。
        self.data.clear();
        self.data.resize(len, 0);

        pack_pbgra32_alpha(&mut self.data, pixels, width, height, stride, row_bytes);

        self.width = width;
        self.height = height;
    }

    /// 指定座標がヒット対象かを判定
    ///
    /// # Returns
    /// - `true`: α ≧ 128 のピクセル(ヒット対象)
    /// - `false`: α < 128 または範囲外
    pub fn is_hit(&self, x: u32, y: u32) -> bool {
        // 範囲外チェック
        if x >= self.width || y >= self.height {
            return false;
        }

        let row_bytes = self.width.div_ceil(8) as usize;
        let byte_index = (y as usize * row_bytes) + (x as usize / 8);
        let bit_index = 7 - (x % 8);

        // 不変条件: well-formed な AlphaMask(from_pbgra32 が生成)は
        // `data.len() == row_bytes * height` を満たし、直前の範囲チェック
        // (x < width, y < height)と併せて byte_index は常に data 範囲内
        // (byte_index <= height*row_bytes - 1 < data.len())。リリースでは
        // この添字は常に安全。debug_assert は data 長と width/height が乖離した
        // 不整合な手構築マスクのみを検出する(リリース挙動は不変)。
        debug_assert!(
            byte_index < self.data.len(),
            "AlphaMask invariant violated: byte_index {} out of bounds (data.len()={}, width={}, height={})",
            byte_index,
            self.data.len(),
            self.width,
            self.height
        );

        (self.data[byte_index] >> bit_index) & 1 == 1
    }

    /// マスク幅を取得
    pub fn width(&self) -> u32 {
        self.width
    }

    /// マスク高さを取得
    pub fn height(&self) -> u32 {
        self.height
    }

    /// 詰めビット列**そのものの確保量**(`Vec::capacity`・バイト)を取得
    ///
    /// [`regenerate_from_pbgra32`] を呼ぶ側が「この再生成で確保が起きたか」を厳密に観測する
    /// ための口である。再生成は `clear`+`resize(詰め長, 0)` ゆえ、詰め長が現容量を超えた
    /// ときにだけ確保が起きる——呼び出しの前後でこの値を読み比べれば、増えた/変わらないが
    /// そのまま「確保した/していない」になる。`Vec` の容量は再確保でしか増えず、縮小方向の
    /// 再生成でも縮まないため、過大にも過小にも振れない。
    ///
    /// [`width`]/[`height`] から詰め長を推し量る形と違い、**この値はマスクの実体に紐づく**。
    /// 複数のマスクを入れ替えて使い回す呼び手(当たり判定マスクの輪番)でも、器側で到達済み
    /// 寸法を覚える必要が無くなる。
    ///
    /// [`regenerate_from_pbgra32`]: AlphaMask::regenerate_from_pbgra32
    /// [`width`]: AlphaMask::width
    /// [`height`]: AlphaMask::height
    pub fn packed_capacity(&self) -> usize {
        self.data.capacity()
    }
}

// Send + Sync は自動導出(Vec<u8> と u32 のみ)

/// `regenerate_from_pbgra32` の等価檻とバッファ再利用檻(structure.md のテスト分離規約)
#[cfg(test)]
#[path = "alpha_mask_regenerate_tests.rs"]
mod regenerate_tests;

#[cfg(test)]
mod tests {
    use super::*;

    /// 透明部分(α < 128)でヒットしないことを確認
    #[test]
    fn test_transparent_pixel_not_hit() {
        // 2x2 PBGRA32: 全て透明(α = 0)
        let pixels = vec![
            0, 0, 0, 0, // (0,0) 透明
            0, 0, 0, 0, // (1,0) 透明
            0, 0, 0, 0, // (0,1) 透明
            0, 0, 0, 0, // (1,1) 透明
        ];
        let mask = AlphaMask::from_pbgra32(&pixels, 2, 2, 8);

        assert!(!mask.is_hit(0, 0));
        assert!(!mask.is_hit(1, 0));
        assert!(!mask.is_hit(0, 1));
        assert!(!mask.is_hit(1, 1));
    }

    /// 不透明部分(α ≧ 128)でヒットすることを確認
    #[test]
    fn test_opaque_pixel_hit() {
        // 2x2 PBGRA32: 全て不透明(α = 255)
        let pixels = vec![
            0, 0, 0, 255, // (0,0) 不透明
            0, 0, 0, 255, // (1,0) 不透明
            0, 0, 0, 255, // (0,1) 不透明
            0, 0, 0, 255, // (1,1) 不透明
        ];
        let mask = AlphaMask::from_pbgra32(&pixels, 2, 2, 8);

        assert!(mask.is_hit(0, 0));
        assert!(mask.is_hit(1, 0));
        assert!(mask.is_hit(0, 1));
        assert!(mask.is_hit(1, 1));
    }

    /// 境界値テスト: α = 127 → ヒットしない
    #[test]
    fn test_threshold_boundary_127() {
        let pixels = vec![0, 0, 0, 127]; // α = 127
        let mask = AlphaMask::from_pbgra32(&pixels, 1, 1, 4);

        assert!(!mask.is_hit(0, 0));
    }

    /// 境界値テスト: α = 128 → ヒットする
    #[test]
    fn test_threshold_boundary_128() {
        let pixels = vec![0, 0, 0, 128]; // α = 128
        let mask = AlphaMask::from_pbgra32(&pixels, 1, 1, 4);

        assert!(mask.is_hit(0, 0));
    }

    /// 範囲外座標でfalseを返すことを確認
    #[test]
    fn test_out_of_bounds() {
        let pixels = vec![0, 0, 0, 255];
        let mask = AlphaMask::from_pbgra32(&pixels, 1, 1, 4);

        assert!(!mask.is_hit(1, 0)); // x = width
        assert!(!mask.is_hit(0, 1)); // y = height
        assert!(!mask.is_hit(100, 100));
    }

    /// 混在パターン: 透明/不透明が混在
    #[test]
    fn test_mixed_pattern() {
        // 2x2: チェッカーパターン
        let pixels = vec![
            0, 0, 0, 255, // (0,0) 不透明
            0, 0, 0, 0, // (1,0) 透明
            0, 0, 0, 0, // (0,1) 透明
            0, 0, 0, 255, // (1,1) 不透明
        ];
        let mask = AlphaMask::from_pbgra32(&pixels, 2, 2, 8);

        assert!(mask.is_hit(0, 0));
        assert!(!mask.is_hit(1, 0));
        assert!(!mask.is_hit(0, 1));
        assert!(mask.is_hit(1, 1));
    }

    /// 8ピクセル以上の幅でビットパックが正しく動作することを確認
    #[test]
    fn test_wide_image_bit_packing() {
        // 10x1: 最初と最後のピクセルが不透明
        let mut pixels = vec![0u8; 10 * 4];
        pixels[3] = 255; // (0,0) 不透明
        pixels[9 * 4 + 3] = 255; // (9,0) 不透明

        let mask = AlphaMask::from_pbgra32(&pixels, 10, 1, 40);

        assert!(mask.is_hit(0, 0));
        for x in 1..9 {
            assert!(!mask.is_hit(x, 0), "x={} should not hit", x);
        }
        assert!(mask.is_hit(9, 0));
    }

    /// サイズアクセサのテスト
    #[test]
    fn test_size_accessors() {
        let pixels = vec![0u8; 100 * 50 * 4];
        let mask = AlphaMask::from_pbgra32(&pixels, 100, 50, 400);

        assert_eq!(mask.width(), 100);
        assert_eq!(mask.height(), 50);
    }

    // === W5b-T 追加: from_pbgra32 / is_hit の境界ロジック特性化 ===

    /// width=0 の退化ケース: 空マスクが生成され、いかなる座標もヒットしない
    #[test]
    fn test_zero_width_produces_empty_mask() {
        let pixels: Vec<u8> = Vec::new();
        let mask = AlphaMask::from_pbgra32(&pixels, 0, 0, 0);

        assert_eq!(mask.width(), 0);
        assert_eq!(mask.height(), 0);
        // 範囲外チェック(x >= 0 が常に真)により必ず false
        assert!(!mask.is_hit(0, 0));
        assert!(!mask.is_hit(5, 5));
    }

    /// height=0(幅あり)の退化ケース: 行が存在せずヒットなし
    #[test]
    fn test_zero_height_produces_empty_mask() {
        let pixels: Vec<u8> = Vec::new();
        let mask = AlphaMask::from_pbgra32(&pixels, 4, 0, 16);

        assert_eq!(mask.width(), 4);
        assert_eq!(mask.height(), 0);
        assert!(!mask.is_hit(0, 0));
        assert!(!mask.is_hit(3, 0));
    }

    /// パディング付き stride: 行末パディングを跨いで各行のαが正しく読まれること
    ///
    /// stride を width*4 より大きく取り(行末に未使用バイト)、
    /// pixel_offset = y*stride + x*4 の stride ベース行頭計算を固定する。
    #[test]
    fn test_padded_stride_reads_correct_rows() {
        // 2x2 画像、stride=12(= 行に必要な 8 バイト + 4 バイトのパディング)
        let stride = 12u32;
        let mut pixels = vec![0u8; (stride * 2) as usize];
        // (0,0) 不透明
        pixels[3] = 255;
        // (1,1) 不透明: 2行目先頭 = y*stride = 12、x=1 → +4、α は +3
        pixels[(1 * stride as usize) + (1 * 4) + 3] = 255;

        let mask = AlphaMask::from_pbgra32(&pixels, 2, 2, stride);

        assert!(mask.is_hit(0, 0), "(0,0) should hit");
        assert!(!mask.is_hit(1, 0), "(1,0) should not hit");
        assert!(!mask.is_hit(0, 1), "(0,1) should not hit");
        assert!(
            mask.is_hit(1, 1),
            "(1,1) should hit (padded stride row offset)"
        );
    }

    /// バイト境界跨ぎのビットパック: x=7(1バイト目末尾)と x=8(2バイト目先頭)
    ///
    /// row_bytes = ceil(width/8)。16px 幅で x=7 と x=8 が別バイト・MSBファースト
    /// で正しく配置されることを固定する。
    #[test]
    fn test_bit_packing_across_byte_boundary() {
        // 16x1: x=7 と x=8 のみ不透明
        let mut pixels = vec![0u8; 16 * 4];
        pixels[7 * 4 + 3] = 255; // x=7(バイト0の bit0)
        pixels[8 * 4 + 3] = 255; // x=8(バイト1の bit7)

        let mask = AlphaMask::from_pbgra32(&pixels, 16, 1, 64);

        for x in 0..16 {
            let expected = x == 7 || x == 8;
            assert_eq!(
                mask.is_hit(x, 0),
                expected,
                "x={x} hit mismatch (boundary packing)"
            );
        }
    }

    /// is_hit が最終有効ピクセル(width-1, height-1)で正しく判定すること
    #[test]
    fn test_is_hit_at_last_valid_pixel() {
        // 3x2: 右下 (2,1) のみ不透明
        let w = 3u32;
        let h = 2u32;
        let stride = w * 4;
        let mut pixels = vec![0u8; (stride * h) as usize];
        pixels[((h - 1) * stride + (w - 1) * 4 + 3) as usize] = 255;

        let mask = AlphaMask::from_pbgra32(&pixels, w, h, stride);

        assert!(mask.is_hit(2, 1), "last valid pixel should hit");
        // 直近の範囲外はヒットしない
        assert!(!mask.is_hit(3, 1), "x == width is out of bounds");
        assert!(!mask.is_hit(2, 2), "y == height is out of bounds");
    }

    // === W5b-V 追加: is_hit 添字不変条件の特性化(debug_assert の保護対象固定) ===

    /// `is_hit` が範囲外座標で添字アクセス手前に return すること(panic 経路なし)
    ///
    /// 非バイト境界幅(width=9 → row_bytes=2)で、範囲外座標が `data[byte_index]` に
    /// 到達せず常に false を返すことを固定する。範囲チェック(x>=width||y>=height)が
    /// debug_assert / 添字の手前で全ての越境を吸収する安全鎖を特性化する。
    #[test]
    fn test_is_hit_out_of_bounds_returns_before_indexing() {
        // 9x3: row_bytes = ceil(9/8) = 2、data.len() = 6
        let w = 9u32;
        let h = 3u32;
        let stride = w * 4;
        let pixels = vec![0u8; (stride * h) as usize];
        let mask = AlphaMask::from_pbgra32(&pixels, w, h, stride);

        // x == width / y == height / 両方超過 / 極大値: いずれも false(panic なし)
        assert!(!mask.is_hit(w, 0));
        assert!(!mask.is_hit(0, h));
        assert!(!mask.is_hit(w, h));
        assert!(!mask.is_hit(u32::MAX, u32::MAX));
        assert!(!mask.is_hit(u32::MAX, 0));
        assert!(!mask.is_hit(0, u32::MAX));
    }

    /// well-formed マスクの全有効座標で `is_hit` が添字不変条件を満たす(debug_assert 非発火)
    ///
    /// 非バイト境界幅(width=17 → row_bytes=3)の最右下を含む全座標を走査し、
    /// `byte_index < data.len()` が常に成立する(debug ビルドでも panic しない)ことを
    /// 固定する。これは is_hit に追加した debug_assert が well-formed マスクで決して
    /// 発火しないこと(=リリース挙動不変)の回帰検知器となる。
    #[test]
    fn test_is_hit_all_valid_coords_satisfy_index_invariant() {
        // 17x5: row_bytes = ceil(17/8) = 3、data.len() = 15。最右下 (16,4) を含む全走査
        let w = 17u32;
        let h = 5u32;
        let stride = w * 4;
        let mut pixels = vec![0u8; (stride * h) as usize];
        // 最右下ピクセルのみ不透明にして最大 byte_index 到達を確実にする
        pixels[((h - 1) * stride + (w - 1) * 4 + 3) as usize] = 255;
        let mask = AlphaMask::from_pbgra32(&pixels, w, h, stride);

        // 全有効座標で is_hit が panic せず評価できる(debug_assert 非発火)
        let mut hit_count = 0u32;
        for y in 0..h {
            for x in 0..w {
                if mask.is_hit(x, y) {
                    hit_count += 1;
                }
            }
        }
        // 最右下のみヒット(不変条件を満たしつつ最大 byte_index が安全に読まれた証拠)
        assert_eq!(hit_count, 1, "only the bottom-right pixel should hit");
        assert!(mask.is_hit(w - 1, h - 1), "max-index pixel should hit");
    }
}