Skip to main content

gitstack/graph/
colors.rs

1//! グラフ色管理
2
3use std::collections::{HashMap, VecDeque};
4
5use ratatui::style::Color;
6
7/// メインブランチの色インデックス(予約)
8pub const MAIN_BRANCH_COLOR: usize = 0;
9
10/// 色パレットのサイズ
11pub const PALETTE_SIZE: usize = 16;
12
13/// グラフで使用する色パレット(16色)
14/// 多数の並列ブランチでも識別しやすいよう拡張
15const GRAPH_PALETTE: [Color; 16] = [
16    Color::Blue,               // 0: メインブランチ用
17    Color::Green,              // 1
18    Color::Yellow,             // 2
19    Color::Magenta,            // 3
20    Color::Cyan,               // 4
21    Color::Red,                // 5
22    Color::LightBlue,          // 6
23    Color::LightGreen,         // 7
24    Color::LightYellow,        // 8
25    Color::LightMagenta,       // 9
26    Color::LightCyan,          // 10
27    Color::LightRed,           // 11
28    Color::Rgb(255, 165, 0),   // 12: Orange
29    Color::Rgb(128, 0, 128),   // 13: Purple
30    Color::Rgb(0, 128, 128),   // 14: Teal
31    Color::Rgb(255, 105, 180), // 15: HotPink
32];
33
34/// 色インデックスからColor値を取得
35pub fn get_graph_color(idx: usize) -> Color {
36    GRAPH_PALETTE[idx % GRAPH_PALETTE.len()]
37}
38
39/// 色割り当て管理
40pub struct ColorAssigner {
41    /// 次に割り当てる色インデックス(0はメインブランチ用に予約)
42    next_idx: usize,
43    /// 使用中の色インデックス
44    used_colors: Vec<bool>,
45}
46
47impl ColorAssigner {
48    pub fn new() -> Self {
49        let mut used_colors = vec![false; GRAPH_PALETTE.len()];
50        used_colors[MAIN_BRANCH_COLOR] = true; // メインブランチ色を予約
51        Self {
52            next_idx: 1,
53            used_colors,
54        }
55    }
56
57    /// メインブランチ用の色を取得
58    pub fn main_color(&self) -> usize {
59        MAIN_BRANCH_COLOR
60    }
61
62    /// 新しい色を割り当て
63    pub fn assign(&mut self) -> usize {
64        // 空いている色を探す
65        for i in 1..GRAPH_PALETTE.len() {
66            let idx = (self.next_idx + i - 1) % GRAPH_PALETTE.len();
67            if idx != MAIN_BRANCH_COLOR && !self.used_colors[idx] {
68                self.used_colors[idx] = true;
69                self.next_idx = (idx + 1) % GRAPH_PALETTE.len();
70                return idx;
71            }
72        }
73        // 全て使用中なら順番に割り当て
74        let idx = if self.next_idx == MAIN_BRANCH_COLOR {
75            1
76        } else {
77            self.next_idx
78        };
79        self.next_idx = (idx + 1) % GRAPH_PALETTE.len();
80        if self.next_idx == MAIN_BRANCH_COLOR {
81            self.next_idx = 1;
82        }
83        idx
84    }
85
86    /// 色を解放
87    pub fn release(&mut self, idx: usize) {
88        if idx != MAIN_BRANCH_COLOR && idx < self.used_colors.len() {
89            self.used_colors[idx] = false;
90        }
91    }
92}
93
94impl Default for ColorAssigner {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100/// 色割り当てのためのコンテキスト情報
101#[derive(Debug, Clone)]
102pub struct ColorContext {
103    /// レーン(列)番号
104    pub lane: usize,
105    /// メインブランチかどうか
106    pub is_main_branch: bool,
107    /// 分岐元コミットのハッシュ(同じ分岐元を持つブランチは異なる色にする)
108    pub fork_point: Option<String>,
109    /// 親コミットの色(継続性のため)
110    pub parent_color: Option<usize>,
111}
112
113impl ColorContext {
114    /// 新しいColorContextを作成
115    pub fn new(lane: usize) -> Self {
116        Self {
117            lane,
118            is_main_branch: false,
119            fork_point: None,
120            parent_color: None,
121        }
122    }
123
124    /// メインブランチフラグを設定
125    pub fn with_main_branch(mut self, is_main: bool) -> Self {
126        self.is_main_branch = is_main;
127        self
128    }
129
130    /// 分岐元を設定
131    pub fn with_fork_point(mut self, fork_point: Option<String>) -> Self {
132        self.fork_point = fork_point;
133        self
134    }
135
136    /// 親の色を設定
137    pub fn with_parent_color(mut self, parent_color: Option<usize>) -> Self {
138        self.parent_color = parent_color;
139        self
140    }
141}
142
143/// ペナルティベースの色割り当て管理
144///
145/// keifuのアルゴリズムを参考に、以下のペナルティを考慮して色を割り当てる:
146/// 1. 隣接レーンペナルティ: 隣接するレーンと同じ色を避ける
147/// 2. 履歴ペナルティ: 最近使用した色を避ける
148/// 3. Fork siblingsペナルティ: 同じ分岐元を持つブランチと同じ色を避ける
149pub struct PenaltyBasedColorAssigner {
150    /// 各レーンに割り当てられた色
151    lane_colors: HashMap<usize, usize>,
152    /// 最近使用した色の履歴
153    color_history: VecDeque<usize>,
154    /// 分岐元ハッシュ→使用中の色リスト
155    fork_colors: HashMap<String, Vec<usize>>,
156    /// 履歴サイズ
157    history_size: usize,
158}
159
160impl PenaltyBasedColorAssigner {
161    /// 新しいPenaltyBasedColorAssignerを作成
162    pub fn new() -> Self {
163        Self {
164            lane_colors: HashMap::new(),
165            color_history: VecDeque::new(),
166            fork_colors: HashMap::new(),
167            history_size: 8,
168        }
169    }
170
171    /// メインブランチ用の色を取得
172    pub fn main_color(&self) -> usize {
173        MAIN_BRANCH_COLOR
174    }
175
176    /// コンテキストを考慮して色を割り当て
177    pub fn assign_with_context(&mut self, ctx: &ColorContext) -> usize {
178        // メインブランチは常に色0
179        if ctx.is_main_branch {
180            self.update_state(ctx.lane, MAIN_BRANCH_COLOR, ctx.fork_point.as_ref());
181            return MAIN_BRANCH_COLOR;
182        }
183
184        // ペナルティ配列を初期化
185        let mut penalties = [0.0f32; PALETTE_SIZE];
186
187        // 1. 隣接レーンペナルティ (weight: 10.0)
188        //    隣接レーンと同じ色は見分けにくいので高ペナルティ
189        for neighbor in [ctx.lane.saturating_sub(1), ctx.lane + 1] {
190            if neighbor != ctx.lane {
191                if let Some(&color) = self.lane_colors.get(&neighbor) {
192                    penalties[color] += 10.0;
193                    // 類似色にも軽いペナルティ
194                    if let Some(similar) = self.similar_color(color) {
195                        penalties[similar] += 5.0;
196                    }
197                }
198            }
199        }
200
201        // 2. 履歴ペナルティ (weight: 3.0 * decay)
202        //    最近使った色は避ける(垂直方向の重複回避)
203        for (age, &color) in self.color_history.iter().enumerate() {
204            let decay = 1.0 - (age as f32 / self.history_size as f32);
205            penalties[color] += 3.0 * decay;
206        }
207
208        // 3. Fork siblingsペナルティ (weight: 8.0)
209        //    同じ分岐元から派生したブランチは異なる色にする
210        if let Some(ref fork) = ctx.fork_point {
211            if let Some(siblings) = self.fork_colors.get(fork) {
212                for &color in siblings {
213                    penalties[color] += 8.0;
214                }
215            }
216        }
217
218        // 4. メインブランチ色(0)には高ペナルティ
219        penalties[MAIN_BRANCH_COLOR] += 100.0;
220
221        // 最小ペナルティの色を選択
222        let best = (1..PALETTE_SIZE)
223            .min_by(|&a, &b| {
224                penalties[a]
225                    .partial_cmp(&penalties[b])
226                    .unwrap_or(std::cmp::Ordering::Equal)
227            })
228            .unwrap_or(1);
229
230        // 状態を更新
231        self.update_state(ctx.lane, best, ctx.fork_point.as_ref());
232
233        best
234    }
235
236    /// 状態を更新
237    fn update_state(&mut self, lane: usize, color: usize, fork_point: Option<&String>) {
238        self.lane_colors.insert(lane, color);
239
240        self.color_history.push_front(color);
241        if self.color_history.len() > self.history_size {
242            self.color_history.pop_back();
243        }
244
245        if let Some(fork) = fork_point {
246            self.fork_colors
247                .entry(fork.clone())
248                .or_default()
249                .push(color);
250        }
251    }
252
253    /// 類似色を取得(同系統の明暗バリエーション)
254    fn similar_color(&self, color: usize) -> Option<usize> {
255        // パレット構成:
256        // 0: Blue, 1: Green, 2: Yellow, 3: Magenta, 4: Cyan, 5: Red
257        // 6: LightBlue, 7: LightGreen, 8: LightYellow, 9: LightMagenta, 10: LightCyan, 11: LightRed
258        // 12: Orange, 13: Purple, 14: Teal, 15: HotPink
259        match color {
260            0 => Some(6), // Blue <-> LightBlue
261            6 => Some(0),
262            1 => Some(7), // Green <-> LightGreen
263            7 => Some(1),
264            2 => Some(8), // Yellow <-> LightYellow
265            8 => Some(2),
266            3 => Some(9), // Magenta <-> LightMagenta
267            9 => Some(3),
268            4 => Some(10), // Cyan <-> LightCyan
269            10 => Some(4),
270            5 => Some(11), // Red <-> LightRed
271            11 => Some(5),
272            _ => None,
273        }
274    }
275
276    /// レーンの色を取得
277    pub fn get_lane_color(&self, lane: usize) -> Option<usize> {
278        self.lane_colors.get(&lane).copied()
279    }
280
281    /// レーンを解放
282    pub fn release_lane(&mut self, lane: usize) {
283        self.lane_colors.remove(&lane);
284    }
285}
286
287impl Default for PenaltyBasedColorAssigner {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_get_graph_color() {
299        assert_eq!(get_graph_color(0), Color::Blue);
300        assert_eq!(get_graph_color(1), Color::Green);
301        assert_eq!(get_graph_color(16), Color::Blue); // ラップアラウンド
302    }
303
304    #[test]
305    fn test_color_assigner_main_color() {
306        let assigner = ColorAssigner::new();
307        assert_eq!(assigner.main_color(), MAIN_BRANCH_COLOR);
308    }
309
310    #[test]
311    fn test_color_assigner_assign_skips_main() {
312        let mut assigner = ColorAssigner::new();
313        let color1 = assigner.assign();
314        assert_ne!(color1, MAIN_BRANCH_COLOR);
315    }
316
317    #[test]
318    fn test_color_assigner_assign_unique() {
319        let mut assigner = ColorAssigner::new();
320        let c1 = assigner.assign();
321        let c2 = assigner.assign();
322        let c3 = assigner.assign();
323        assert_ne!(c1, c2);
324        assert_ne!(c2, c3);
325        assert_ne!(c1, c3);
326    }
327
328    #[test]
329    fn test_color_assigner_release() {
330        let mut assigner = ColorAssigner::new();
331        let c1 = assigner.assign();
332        let _c2 = assigner.assign();
333        assigner.release(c1);
334        // 解放後は再利用可能(次回割り当て時に再利用される)
335        let c3 = assigner.assign();
336        // c1が解放されているので再利用されるはず
337        assert!(!assigner.used_colors[c1] || c3 == c1);
338    }
339
340    // PenaltyBasedColorAssigner tests
341
342    #[test]
343    fn test_penalty_assigner_main_color() {
344        let assigner = PenaltyBasedColorAssigner::new();
345        assert_eq!(assigner.main_color(), MAIN_BRANCH_COLOR);
346    }
347
348    #[test]
349    fn test_penalty_assigner_main_branch_always_color_0() {
350        let mut assigner = PenaltyBasedColorAssigner::new();
351        let ctx = ColorContext::new(0).with_main_branch(true);
352        let color = assigner.assign_with_context(&ctx);
353        assert_eq!(color, MAIN_BRANCH_COLOR);
354    }
355
356    #[test]
357    fn test_penalty_assigner_skips_main_color() {
358        let mut assigner = PenaltyBasedColorAssigner::new();
359        let ctx = ColorContext::new(1);
360        let color = assigner.assign_with_context(&ctx);
361        assert_ne!(color, MAIN_BRANCH_COLOR);
362    }
363
364    #[test]
365    fn test_penalty_assigner_adjacent_lanes_different_colors() {
366        let mut assigner = PenaltyBasedColorAssigner::new();
367
368        // レーン0に色を割り当て
369        let ctx0 = ColorContext::new(0);
370        let color0 = assigner.assign_with_context(&ctx0);
371
372        // レーン1は隣接するので異なる色になるはず
373        let ctx1 = ColorContext::new(1);
374        let color1 = assigner.assign_with_context(&ctx1);
375
376        assert_ne!(color0, color1, "隣接レーンは異なる色になるべき");
377    }
378
379    #[test]
380    fn test_penalty_assigner_fork_siblings_different_colors() {
381        let mut assigner = PenaltyBasedColorAssigner::new();
382        let fork_point = "base_commit".to_string();
383
384        // 同じ分岐元から派生したブランチ
385        let ctx1 = ColorContext::new(1).with_fork_point(Some(fork_point.clone()));
386        let color1 = assigner.assign_with_context(&ctx1);
387
388        let ctx2 = ColorContext::new(2).with_fork_point(Some(fork_point.clone()));
389        let color2 = assigner.assign_with_context(&ctx2);
390
391        let ctx3 = ColorContext::new(3).with_fork_point(Some(fork_point));
392        let color3 = assigner.assign_with_context(&ctx3);
393
394        assert_ne!(color1, color2, "Fork siblingsは異なる色になるべき");
395        assert_ne!(color2, color3, "Fork siblingsは異なる色になるべき");
396        assert_ne!(color1, color3, "Fork siblingsは異なる色になるべき");
397    }
398
399    #[test]
400    fn test_penalty_assigner_history_avoidance() {
401        let mut assigner = PenaltyBasedColorAssigner::new();
402
403        // 複数の色を連続で割り当て
404        let mut colors = Vec::new();
405        for i in 0..5 {
406            let ctx = ColorContext::new(i * 3); // 離れたレーンを使用
407            let color = assigner.assign_with_context(&ctx);
408            colors.push(color);
409        }
410
411        // 最初の数色は重複しないはず(履歴ペナルティにより)
412        for i in 0..4 {
413            for j in (i + 1)..5 {
414                if i + 1 < j {
415                    // 連続していない場合は重複しにくい
416                    // (完全な重複回避は保証されないが、傾向として確認)
417                }
418            }
419        }
420        // 少なくとも全部同じ色にはならない
421        let unique_count = colors
422            .iter()
423            .collect::<std::collections::HashSet<_>>()
424            .len();
425        assert!(unique_count >= 3, "履歴により色のバリエーションがあるべき");
426    }
427
428    #[test]
429    fn test_penalty_assigner_get_lane_color() {
430        let mut assigner = PenaltyBasedColorAssigner::new();
431        let ctx = ColorContext::new(5);
432        let color = assigner.assign_with_context(&ctx);
433
434        assert_eq!(assigner.get_lane_color(5), Some(color));
435        assert_eq!(assigner.get_lane_color(0), None);
436    }
437
438    #[test]
439    fn test_penalty_assigner_release_lane() {
440        let mut assigner = PenaltyBasedColorAssigner::new();
441        let ctx = ColorContext::new(3);
442        let _color = assigner.assign_with_context(&ctx);
443
444        assert!(assigner.get_lane_color(3).is_some());
445        assigner.release_lane(3);
446        assert!(assigner.get_lane_color(3).is_none());
447    }
448
449    #[test]
450    fn test_color_context_builder() {
451        let ctx = ColorContext::new(5)
452            .with_main_branch(true)
453            .with_fork_point(Some("abc123".to_string()))
454            .with_parent_color(Some(3));
455
456        assert_eq!(ctx.lane, 5);
457        assert!(ctx.is_main_branch);
458        assert_eq!(ctx.fork_point, Some("abc123".to_string()));
459        assert_eq!(ctx.parent_color, Some(3));
460    }
461
462    #[test]
463    fn test_penalty_assigner_many_lanes() {
464        let mut assigner = PenaltyBasedColorAssigner::new();
465
466        // 16レーン以上を割り当て
467        for i in 0..20 {
468            let ctx = ColorContext::new(i);
469            let color = assigner.assign_with_context(&ctx);
470            // 色はパレットサイズ内に収まる
471            assert!(color < PALETTE_SIZE, "色インデックスがパレットサイズ内");
472        }
473    }
474}