Skip to content

feat: ズーム時にミニマップを表示する機能を追加#12

Merged
flexphere merged 2 commits intomainfrom
feat/minimap
Mar 8, 2026
Merged

feat: ズーム時にミニマップを表示する機能を追加#12
flexphere merged 2 commits intomainfrom
feat/minimap

Conversation

@flexphere
Copy link
Owner

@flexphere flexphere commented Mar 8, 2026

概要

resolves #1

  • ズーム時(ZoomLevel > 1.0)に画面右下にミニマップを表示する機能を追加
  • ミニマップは画像全体の縮小サムネイルに、現在表示中の領域を示す矩形インジケータを重ねて表示
  • ボーダー色は TOML 設定ファイルで変更可能(デフォルト: #FFFFFF

変更内容

  • domain: IsZoomed() メソッド、MinimapConfig 構造体を追加
  • usecase: RendererPort にミニマップ用メソッド3つ追加、RenderFrameUseCase でミニマップ描画ロジックを実装
  • adapter/renderer: KittyRenderer にミニマップ描画実装(CatmullRom 補間でダウンスケール、矩形インジケータをイメージ上に直接描画、全エスケープシーケンスをアトミックに出力)
  • adapter/config: TOML ローダーにミニマップ設定(enabled, size, border_color)を追加
  • cmd/gaze: DI 配線とクリーンアップ処理を更新

設定例

[minimap]
enabled = true
size = 0.2
border_color = "#00FF00"

テスト計画

  • IsZoomed() の境界値テスト(6ケース)
  • MinimapConfig デフォルト値テスト
  • ミニマップ有効/無効、ズーム状態、小端末スキップ、1回のみアップロード、エラーパスのテスト(8件)
  • KittyRenderer ミニマップメソッドテスト(10件: DisplayMinimap、UploadMinimap、カーソル位置、エッジケース、parseHexColor、clampInt)
  • make ci 全チェックパス

🤖 Generated with Claude Code

flexphere and others added 2 commits March 8, 2026 23:14
Show a thumbnail overview with viewport indicator at bottom-right
when the image is zoomed in. The minimap is rendered by compositing
the indicator rectangle directly onto a downscaled thumbnail image
each frame, with all Kitty escape sequences output atomically to
prevent timing issues. Border color is configurable via TOML config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover IsZoomed, MinimapConfig defaults, minimap rendering logic
(enabled/disabled, zoom state, small terminal skip, upload-once,
error paths), and KittyRenderer minimap methods (DisplayMinimap,
UploadMinimap, cursor positioning, edge cases, parseHexColor, clampInt).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 14:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

ズーム時(ZoomLevel > 1.0)に画面右下へミニマップ(縮小サムネイル+表示領域インジケータ)を描画できるようにし、設定(TOML)で有効化・サイズ・枠線色を調整できるようにするPRです。

Changes:

  • domain に Viewport.IsZoomed()MinimapConfig、デフォルト設定を追加
  • usecase にミニマップ描画フロー(Upload/Display)と RendererPort 拡張を追加
  • KittyRenderer にミニマップ生成・インジケータ描画・アトミック出力、TOML ローダーと cmd/gaze のDI/クリーンアップを更新

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/usecase/render_frame.go ズーム時のミニマップ描画をRenderFrameに統合
internal/usecase/render_frame_test.go RenderFrameUseCase のミニマップ分岐のテスト追加
internal/usecase/ports.go RendererPort にミニマップ用メソッドを追加
internal/domain/viewport.go IsZoomed() を追加
internal/domain/viewport_test.go IsZoomed() の境界テスト追加
internal/domain/config.go MinimapConfig とデフォルト値を追加
internal/domain/config_test.go ミニマップデフォルト値のテスト追加
internal/adapter/renderer/kitty_renderer.go Kittyでのミニマップ生成・描画・削除を実装
internal/adapter/renderer/kitty_renderer_test.go KittyRenderer のミニマップ関連テスト追加
internal/adapter/config/toml_loader.go TOMLからミニマップ設定を読み込むよう拡張
cmd/gaze/main.go MinimapConfig をDIし、終了時にミニマップもクリーンアップ

Comment on lines +158 to +162
// Copy base thumbnail
bounds := r.minimapBase.Bounds()
frame := image.NewRGBA(bounds)
copy(frame.Pix, r.minimapBase.Pix)

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisplayMinimap() で r.minimapBase を毎フレーム image.NewRGBA で新規確保してコピーしており、フレームレートが高い環境ではGC負荷が増えます。ミニマップ用の作業バッファ(*image.RGBA)を KittyRenderer に保持して再利用する、または Pix スライスを再利用して copy するなど、割り当て回数を減らす改善を検討してください。

Copilot uses AI. Check for mistakes.
pxTop = clampInt(pxTop, 0, r.minimapH-1)
pxRight = clampInt(pxRight, 1, r.minimapW)
pxBottom = clampInt(pxBottom, 1, r.minimapH)

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VisibleRect() の丸め次第で pxRight==pxLeft / pxBottom==pxTop になり得ます(高ズーム時など)。その場合 drawRectBorder の描画が不安定になり、インジケータが消える/崩れる可能性があります。クランプ後に right<=left のときは right=left+1、bottom<=top のときは bottom=top+1 など、最小1px以上の矩形になるよう補正してください(上限も bounds 内に再クランプ)。

Suggested change
// Ensure the rectangle has at least 1px width/height after clamping.
if pxRight <= pxLeft {
pxRight = pxLeft + 1
}
if pxBottom <= pxTop {
pxBottom = pxTop + 1
}
// Re-clamp to bounds to keep the indicator fully inside the minimap image.
if pxRight > bounds.Max.X {
pxRight = bounds.Max.X
}
if pxBottom > bounds.Max.Y {
pxBottom = bounds.Max.Y
}

Copilot uses AI. Check for mistakes.
Comment on lines +332 to +358
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return fmt.Errorf("encoding image to PNG: %w", err)
}

encoded := base64.StdEncoding.EncodeToString(buf.Bytes())

const chunkSize = 4096
for i := 0; i < len(encoded); i += chunkSize {
end := i + chunkSize
if end > len(encoded) {
end = len(encoded)
}
chunk := encoded[i:end]

more := 1
if end >= len(encoded) {
more = 0
}

if i == 0 {
fmt.Printf("\x1b_Gi=%d,f=100,a=t,t=d,m=%d;%s\x1b\\", id, more, chunk)
} else {
fmt.Printf("\x1b_Gi=%d,m=%d;%s\x1b\\", id, more, chunk)
}
}

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildUploadSequence() と uploadImage() で PNGエンコード→base64→4096byte分割の処理が重複しています。片方をもう片方に寄せて共通化(例: buildUploadSequence を使って uploadImage は fmt.Print するだけにする)すると、将来のプロトコルパラメータ変更時の修正漏れを防げます。

Suggested change
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return fmt.Errorf("encoding image to PNG: %w", err)
}
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
const chunkSize = 4096
for i := 0; i < len(encoded); i += chunkSize {
end := i + chunkSize
if end > len(encoded) {
end = len(encoded)
}
chunk := encoded[i:end]
more := 1
if end >= len(encoded) {
more = 0
}
if i == 0 {
fmt.Printf("\x1b_Gi=%d,f=100,a=t,t=d,m=%d;%s\x1b\\", id, more, chunk)
} else {
fmt.Printf("\x1b_Gi=%d,m=%d;%s\x1b\\", id, more, chunk)
}
}
seq, err := buildUploadSequence(id, img)
if err != nil {
return err
}
fmt.Print(seq)

Copilot uses AI. Check for mistakes.
Comment on lines +78 to 82
if err := kittyRenderer.ClearMinimap(); err != nil {
return fmt.Errorf("clearing minimap: %w", err)
}
if err := kittyRenderer.Clear(); err != nil {
return fmt.Errorf("clearing renderer: %w", err)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

クリーンアップ処理で ClearMinimap() が失敗すると、その後の Clear() が呼ばれず端末上にメイン画像が残る可能性があります。両方のクリーンアップは可能な限り実行し、エラーはまとめて返す(例: 先に Clear() も試してから複合エラーにする)形にすると後片付けが確実になります。

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +57
// Append minimap when zoomed in and enabled
if uc.minimapCfg.Enabled && vp.IsZoomed() {
minimapCols, minimapRows := uc.minimapSize(vp)
if minimapCols >= 5 && minimapRows >= 3 {
if !uc.minimapUploaded {
if err := uc.renderer.UploadMinimap(img, minimapCols, minimapRows); err != nil {
return "", fmt.Errorf("uploading minimap: %w", err)
}
uc.minimapUploaded = true
}

mmOutput, err := uc.renderer.DisplayMinimap(vp, minimapCols, minimapRows, uc.minimapCfg.BorderColor)
if err != nil {
return "", fmt.Errorf("displaying minimap: %w", err)
}
output += mmOutput
}
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ミニマップを一度表示した後にズーム解除(ZoomLevel<=1.001)や端末が小さい等で条件を満たさなくなっても、ここでは ClearMinimap() が呼ばれないため、前フレームのミニマップが端末上に残り続ける可能性があります。前回ミニマップを表示していた場合は、非表示条件になったタイミングで renderer.ClearMinimap() を呼ぶ(必要なら表示状態を uc 側で保持する)ようにして、ズームアウト時に確実に消えるようにしてください。

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +44
if minimapCols >= 5 && minimapRows >= 3 {
if !uc.minimapUploaded {
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimapCols>=5 / minimapRows>=3 の閾値がマジックナンバーになっており、仕様変更時に追従漏れが起きやすいです。意味(例: 最小表示可能サイズ)を表す定数にして名前付けするか、MinimapConfig 側で最小値として設定可能にすることを検討してください。

Copilot uses AI. Check for mistakes.
cfg.Minimap.Enabled = *tc.Minimap.Enabled
}
if tc.Minimap.Size != nil {
cfg.Minimap.Size = *tc.Minimap.Size
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimapCfg.Size がTOMLからそのまま取り込まれており、範囲外(負値や極端に大きい値)でも受け入れてしまいます。その結果、描画側で cols/rows が過大になってメモリアロケーションが膨らむ・表示が崩れるなどのリスクがあります。ここで Size を 0<Size<=1 にクランプする、もしくは不正値の場合はデフォルトにフォールバック/エラーにする等のバリデーションを追加してください。

Suggested change
cfg.Minimap.Size = *tc.Minimap.Size
size := *tc.Minimap.Size
if size > 0 && size <= 1 {
cfg.Minimap.Size = size
}

Copilot uses AI. Check for mistakes.
// UploadMinimap creates a downscaled thumbnail base image for the minimap.
// The base is kept in memory; actual upload happens in DisplayMinimap each frame
// (with the viewport indicator rectangle drawn on top).
func (r *KittyRenderer) UploadMinimap(img *domain.ImageEntity, cols, rows int) error {
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UploadMinimap() を複数回呼んだ場合に、既存の minimapID を削除せず新しいIDへ上書きするため、端末側に古いミニマップ画像が残ってリークする可能性があります(ClearMinimap() は最新IDしか消しません)。新しいIDを採番する前に既存IDを削除する、または既存IDを再利用するなどして後始末を入れてください。

Suggested change
func (r *KittyRenderer) UploadMinimap(img *domain.ImageEntity, cols, rows int) error {
func (r *KittyRenderer) UploadMinimap(img *domain.ImageEntity, cols, rows int) error {
// 既存の minimap 画像があれば、端末側から削除してから新しい ID を採番する
if r.minimapID > 0 {
fmt.Printf("\x1b_Ga=d,d=i,i=%d\x1b\\", r.minimapID)
}

Copilot uses AI. Check for mistakes.
@flexphere flexphere marked this pull request as ready for review March 8, 2026 15:14
@flexphere flexphere merged commit 83dd278 into main Mar 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zoom時にminimapを表示したい

2 participants