# プレビュー向きとクロップ仕様書 (Preview Orientation & Crop Specification)

本ドキュメントでは，SmTIAS-Capture のプレビュー表示における **向き変換（反転）** と
**クロップ拡大** の仕組み，座標系の定義，および変更時の確認手順を定義する．

- 画面・UI は [SPEC_01 画面機能仕様書](SPEC_01_画面機能仕様書.md) を参照
- カメラ制御・撮影フロー・保存ファイルは [SPEC_02 カメラ撮影仕様書](SPEC_02_カメラ撮影仕様書.md) を参照
- 本書はプレビュー表示の向き・クロップについて SPEC_02「プレビュー表示」の記述を**詳細化・優先**する

> **重要: 撮影画像（DNG）には影響しない．** 本書が扱う向き変換・クロップは
> すべて**プレビュー表示のみ**の処理である．保存される DNG は別経路（Camera2）で取得する．

---

## 1. 背景: なぜ向き変換が必要か

スマホを**画面が下向き**になるようにアタッチメントへ載せて口元を撮影する運用のため，
カメラの生プレビューをそのまま表示すると，撮影者から見て上下・左右が反転している．
そこで「撮影者が自然に見える向き」に整える変換を表示時に適用する．

正しい表示状態（撮影者が見る向き）の特徴:

- 口元が中央に拡大表示される
- 上部のアタッチメントラベル「SmTIAS 001」が**鏡像＋上下反転**で写る（＝向き変換が効いている証拠）
- カラーチャートが画面右，位置合わせマーカーが四隅

> **基準スクリーンショットについて:** 実機の基準画像は被写体（顔・口元）の
> 個人情報を含むため Git にはコミットせず，`.gitignore` 除外の `local_reference/`
> にローカル保管する．確認時はその画像と見比べる（§5）．

---

## 2. 座標系の定義（用語の統一）

本プロジェクトでは以下の2つの座標系を区別する．コード内コメントもこの用語に統一する．

| 用語 | 意味 |
| --- | --- |
| **生フレーム座標** | カメラから来たままの向き（向き変換適用前）．`OrientedPreview` の入力． |
| **表示後座標** | 向き変換を適用し，撮影者が見る向きにした後の座標（正規化 0.0–1.0）．`PreviewRegion` / `RegionCrop` が扱う座標系． |

**`PreviewRegion` のクロップ座標は必ず「表示後座標」で定義する．**
（向き変換の後に切り出すため．生フレーム座標ではない．）

---

## 3. パイプライン全体図

プレビューには **ライブ系（描画）** と **スナップショット系（保存中表示）** の2系統があり，
**いずれも最終的に同じ `RegionCrop`（同じ `region`）でクロップ**して表示を一致させる．

```text
                         ┌─ ライブプレビュー系 (Flutter 描画) ───────────┐
                         │  AspectRatio（h/w で横→縦補正）               │
生カメラフレーム          │  OrientedPreview（向き変換 = 180°回転）        │
(生フレーム座標)      ────┤    oriented_preview.dart                     │── RegionCrop(region)
                         │  ※ camera_preview.dart                       │   表示後座標で
                         └──────────────────────────────────────────────┘   クロップ＆拡大
                         ┌─ スナップショット系 (native 変換) ────────────┐      ▲
最新フレーム             │  convertYuvToJpeg(rotation:90, mirror:true)   │      │ 同じ region・
(_latestFrame)       ────┤    camera_provider.dart                      │── CroppedSnapshot
                         │  = 向き処理済みの JPEG（表示後座標）            │   → RegionCrop(region)
                         └──────────────────────────────────────────────┘──────┘
```

### 向き変換が2系統に分かれている点（最重要）

**同じ「撮影者が見る向き」を作るのに，入力が異なる2系統で別々の手段を使っている．**
ただし両系統の値は **`lib/models/display_orientation.dart` の `DisplayOrientation` に
一元化**されており，変更窓口は1箇所である．

| 系統 | 向き処理 | 派生元（単一定義） | 適用箇所 |
| --- | --- | --- | --- |
| ライブ（Flutter 描画） | `Matrix4.diagonal3Values(-1, -1, 1)`（X 反転＋Y 反転＝180°回転） | `DisplayOrientation.previewMatrix` | `lib/widgets/oriented_preview.dart` |
| スナップショット（native） | `rotation:90 + mirror:true`（Kotlin 側で回転・ミラー） | `DisplayOrientation.snapshotRotationDegrees` / `.snapshotMirror` | `lib/providers/camera_provider.dart`（`convertYuvToJpeg`） |

> **向きを変更したいときは，`DisplayOrientation.attachmentDownward` の値だけを変更する．**
> ライブ系とスナップショット系は適用対象（入力空間）が異なるため，1つの定義から
> 派生していても**幾何的な整合は実機でしか保証できない**．
> 変更後は必ず §5 の確認手順（段階1 自動 ＋ 段階2 実機）を実施すること．

---

## 4. クロップ拡大の仕組み（RegionCrop）

`lib/widgets/region_crop.dart` が表示後座標 0.0–1.0 の矩形 `PreviewRegion` を受け取り，
以下を行う（ライブ・スナップショット共通）．

1. ROI（関心領域）の中心を画面中心へ平行移動
2. ROI 全体が画面に収まる `contain` ズーム（縦横同一スケールで歪み防止）
3. `ClipRect` ではみ出しを切り取る

クロップ領域は `assets/config/preview_region.json` から読み込み，
読み込み失敗・不正値の場合は `PreviewRegion.full`（全画面・クロップなし）にフォールバックする
（`lib/services/preview_region_service.dart`）．
領域の調整には `scripts/preview_region_picker.html` を用いる．

---

## 5. 変更時の確認手順（必須）

向き変換・クロップに関わる変更を行ったら，**2段階**で確認する．

### 段階1: 自動チェック（リファクタ前後で見た目が変わっていないか）

```bash
flutter test test/widgets/preview_orientation_golden_test.dart
```

- 合成テストパターン（上=赤/下=青/左=緑/右=黄＋左上マーカー円）を
  `OrientedPreview`・`RegionCrop` に流し，正解ゴールデン画像と照合する
- 反転や座標を取り違えると色帯・円の位置がズレて**即失敗**する
- これは「変換を壊していないか」の高速チェックであり，実機は不要

> **意図的に向きを変更した場合**は，ゴールデンが古い正解のままなので失敗する．
> その場合のみ `flutter test --update-goldens` で正解を更新し，
> 更新後のゴールデン画像が意図通りかを目視確認してからコミットする．

### 段階2: 実機での最終確認（向きが本当に正しいか）

- 実機で口元を撮影し，**画面全体のスクリーンショット**を撮る
- ローカル保管の基準画像（`local_reference/` 配下，個人情報のため非コミット）
  と並べて目視で一致を確認する
- 確認観点:
  - 口元が中央に拡大表示されているか
  - 上部ラベル「SmTIAS 001」が鏡像＋上下反転で写るか
  - カラーチャートが右，マーカーが四隅にあるか
  - 上下・左右が実際の口の向きと一致するか

> 自動チェック（段階1）は「変わっていないこと」を守り，
> 実機確認（段階2）は「そもそも正しいこと」を守る．役割が異なるため両方行う．

---

## 6. 関連ファイル

| ファイル | 役割 |
| --- | --- |
| `lib/models/preview_region.dart` | クロップ領域（表示後座標）の不変モデル |
| `lib/services/preview_region_service.dart` | `preview_region.json` の読み込み・フォールバック |
| `lib/models/display_orientation.dart` | **向き設定の単一定義**（ライブ・スナップショット両系統の派生元） |
| `lib/widgets/oriented_preview.dart` | 向き変換の適用（ライブ系，`DisplayOrientation.previewMatrix`） |
| `lib/widgets/region_crop.dart` | クロップ拡大（ライブ・スナップショット共通） |
| `lib/widgets/camera_preview.dart` | ライブプレビュー（向き変換＋クロップ） |
| `lib/widgets/cropped_snapshot.dart` | 保存中スナップショット表示（クロップのみ） |
| `lib/providers/camera_provider.dart` | スナップショット系の向き処理（`rotation:90, mirror:true`） |
| `assets/config/preview_region.json` | クロップ領域の設定値 |
| `scripts/preview_region_picker.html` | クロップ領域の調整ツール |
| `test/models/display_orientation_test.dart` | 向き派生値の契約テスト（段階1，native パラメータの固定） |
| `test/widgets/preview_orientation_golden_test.dart` | 向き・クロップの回帰テスト（段階1） |
| `local_reference/`（非コミット） | 実機確認の基準スクリーンショット保管場所（段階2，個人情報のため Git 除外） |
