# Zoom / Pan / Clamp Mekanizması — Referans Doküman

**Dosya:** `lib/widgets/image_canvas.dart`
**Son güncelleme:** 23 Temmuz 2026

---

## 1. Mimari Karar: Neden InteractiveViewer Yok?

Başlangıçta `InteractiveViewer` kullanılıyordu. Ancak InteractiveViewer'ın built-in pan sınırlaması (`boundaryMargin`) çocuk widget'ın **tam boyutuna** göre çalışır. Fit modda `Center` kullanılınca, Center widget'ı viewport boyutunda olduğu halde içindeki Image daha küçüktür. IV, Image'ın değil Center'ın kenarlarına göre sınırlama yapar → Image'ın etrafında boşluk kalır.

**Çözüm:** InteractiveViewer tamamen kaldırıldı. Yerine:

```
ClipRect → GestureDetector → Listener → Transform → Center/SizedBox → Image
```

- `Transform`: zoom/pan uygular (`_tx`, `_ty`, `_sc`)
- `ClipRect`: taşan kısımları kırpar
- `GestureDetector`: pan (drag) algılar
- `Listener`: scroll (tekerlek) algılar

---

## 2. Durum Değişkenleri

```dart
double _tx = 0;    // viewport-space X kaydırma (piksel)
double _ty = 0;    // viewport-space Y kaydırma (piksel)
double _sc = 1.0;  // ölçek (1.0 = orijinal boyut)
bool _isFilled;    // true=fill mod, false=fit mod
Size _vpSize;      // LayoutBuilder'dan alınan viewport boyutu
```

**Dönüşüm matrisi** (Transform widget'ına verilen):

```
M = T(_tx, _ty) · S(_sc)
```

Yani: child-space'deki (cx, cy) noktası → viewport-space: `vx = cx * _sc + _tx`

---

## 3. Widget Ağacı (build metodu, satır 111–154)

```
LayoutBuilder
  ClipRect
    GestureDetector
      onDoubleTap → _toggleFillFit()
      onPanUpdate → _onPanUpdate()
      Listener
        onPointerSignal → _handleScroll()
        Transform(translate(_tx,_ty) · scale(_sc))
          _isFilled ?
            SizedBox.expand(Image.file(fit: BoxFit.cover))    // FILL
          : Center(Image.file(fit: BoxFit.contain))            // FIT
```

- Viewport boyutu (`_vpSize`), `LayoutBuilder`'ın `constraints.biggest`'inden okunur (satır 115). Her build'te güncellenir. `context.findRenderObject()` kullanılmaz çünkü LayoutBuilder'ın verdiği constraints daha güvenilirdir.
- `_imageKey`, `Image.file` widget'ına bağlıdır. Sadece errorBuilder için kullanılır (clamping'te kullanılmaz).

---

## 4. Scroll Zoom Mekanizması (satır 50–71)

```
_handleScroll(PointerScrollEvent e)
```

```dart
// 1. İmleci viewport-space'den child-space'e çevir
cx = (vx - _tx) / _sc
cy = (vy - _ty) / _sc

// 2. Yeni ölçek (her tick'te %10)
factor = 1.1
newSc = (_sc * (tekerlek_yönüne_göre factor veya 1/factor)).clamp(0.1, 10.0)

// 3. Zoom'u imleç merkezli uygula
_tx = _tx + cx * _sc - cx * newSc
_ty = _ty + cy * _sc - cy * newSc
_sc = newSc
```

**Formülün türetilmesi:** Zoom öncesi imleç altındaki child-space noktası `(cx, cy)`, viewport'ta `(vx, vy)` konumundadır. Zoom sonrası aynı noktanın yine `(vx, vy)`'de kalması için:

```
vx = _tx_old + cx * _sc_old    (zoom öncesi)
vx = _tx_new + cx * _sc_new    (zoom sonrası, aynı nokta)
→ _tx_new = vx - cx * _sc_new
→ _tx_new = (_tx_old + cx * _sc_old) - cx * _sc_new
→ _tx_new = _tx_old + cx * _sc_old - cx * _sc_new
```

---

## 5. Pan Mekanizması (satır 75–80)

```
_onPanUpdate(DragUpdateDetails d)
```

```dart
_tx += d.delta.dx;   // her drag update'te delta biriktir
_ty += d.delta.dy;
if (_isFilled) _clampFill(_viewport());
setState(() {});
```

`d.delta`, son update'ten beri olan değişimi verir (toplam değil). Her update'te delta eklenir, clamp yapılır, setState ile yeniden çizilir.

---

## 6. Clamp Sistemi — SADECE Fill Mod (satır 84–107)

Fit modda hiçbir sınırlama yoktur. Kullanıcı özgürce pan/zoom yapabilir.

### `_clampFill(Size vp)`

```
imgL = _tx               → Image'ın viewport-space sol kenarı
imgR = _tx + vpw * _sc   → Image'ın viewport-space sağ kenarı
vw = imgR - imgL          → Image'ın viewport-space genişliği
```

**Mantık:** Fill modda `SizedBox.expand(BoxFit.cover)` kullanıldığı için Image her zaman viewport'u tam doldurur. Yani Image'ın child-space genişliği = vpw. Ölçeklenmiş genişliği = `vpw * _sc`.

**Yatay sınırlama:**
```
if (vw > vpw + 0.5)          // Image viewport'tan geniş mi?
    if (imgL > 0)            // sol kenar viewport içinde → sola çek
        _tx -= imgL
    else if (imgR < vpw)     // sağ kenar viewport içinde → sağa çek
        _tx += vpw - imgR
```

**Dikey sınırlama:** Aynı mantık `imgT`/`imgB` ve `vph` ile.

**Epsilon (`0.5`):** Floating-point hatalarını önlemek için. Tam eşitlik durumunda (vw = vpw) hiçbir işlem yapılmaz.

---

## 7. Fill / Fit Geçişi (satır 39–45)

```
_toggleFillFit()
```

```dart
_isFilled = !_isFilled;
_tx = 0; _ty = 0; _sc = 1.0;  // sıfırla
```

Çift tıklandığında:
- Mod değiştirilir
- Transform sıfırlanır (identity)
- Viewport cache'i sıfırlanır (sonraki build'te yeniden okunur)

Fill → Fit: Image Center içinde BoxFit.contain ile ortalanır, sınırlama yok.
Fit → Fill: Image SizedBox.expand içinde BoxFit.cover ile doldurur, sınırlama aktif.

---

## 8. Viewport Boyutu (satır 46, 115)

```dart
Size _viewport() => _vpSize;
```

`_vpSize` değeri `LayoutBuilder`'ın `builder` callback'inde `constraints.biggest` olarak alınır (satır 115). Her build'te güncellenir. Neden `context.findRenderObject()` değil?

- `LayoutBuilder`'ın constraints'i, widget'ın parent'tan aldığı en güncel boyut bilgisidir
- `context.findRenderObject()?.size` bazen null veya eski değer döndürebilir (henüz layout olmamışsa)
- `constraints.biggest` her zaman doğru ve günceldir

---

## 9. Görüntü Boyutu Nereden Alınıyor?

**Fill mod:** `SizedBox.expand(BoxFit.cover)` → Image viewport'u tam doldurur. Görüntü boyutu = viewport boyutu. Clamp'ta `vp.width` ve `vp.height` kullanılır.

**Fit mod:** `Center(BoxFit.contain)` → Image fitted boyutta çizilir. Clamp OLMADIĞI için görüntü boyutuna ihtiyaç yoktur.

Eski versiyonlarda `_imageKey.currentContext?.findRenderObject()` ile render box okunuyordu. Bunun iki sorunu vardı:
1. `frameBuilder` içinde çağrılınca render object henüz oluşmamış olabiliyor → null veya 0 boyut dönüyor
2. `SizedBox.expand` içinde render box viewport boyutunu döndürüyor (fitted boyutu değil)

---

## 10. Özet: Hangi Fonksiyon Ne Yapıyor?

| Fonksiyon | Satır | Görevi |
|-----------|-------|--------|
| `_handleScroll()` | 50 | Mouse tekerleği ile zoom (imleç merkezli) |
| `_onPanUpdate()` | 75 | Sürükleme ile pan |
| `_clampFill()` | 84 | Fill modda kenar sınırlaması |
| `_toggleFillFit()` | 39 | Fill/Fit geçişi ve sıfırlama |
| `_resetView()` | 33 | Yeni resim açıldığında sıfırlama |
| `_viewport()` | 46 | Viewport boyutunu döndürür |
| `build()` | 112 | Widget ağacını kurar, `_vpSize`'ı günceller |

---

## 11. Widget Tree (görsel)

```
ImageCanvas
 └─ LayoutBuilder ─(constraints.biggest→_vpSize)─
    └─ ClipRect
       └─ GestureDetector (doubleTap, panUpdate)
          └─ Listener (onPointerSignal→scroll)
             └─ Transform [T(_tx,_ty)·S(_sc)]
                └─ [_isFilled ? SizedBox.expand : Center]
                   └─ Image.file(key:_imageKey, fit:cover/contain)
```

---

## 12. Geçmiş / Alınan Dersler

1. **InteractiveViewer çıkarıldı** — boundaryMargin çocuk widget'ın tam boyutuna göre çalışıyor, Center içindeki Image'ın asıl boyutunu bilmiyor.

2. **Center yerine SizedBox.expand denenmesi** — `_baseContentRect` ile fitted boyut hesaplandı ama `_intrinsicSize` asenkron yükleniyor, ilk frame'lerde yanlış boyut kullanılıyordu.

3. **Render box'tan boyut okuma** — `frameBuilder` içinde render object henüz oluşmamış olabiliyor (build → layout sırası). Retry mekanizması eklendi ama güvenilir olmadı.

4. **Fit modda sınırlamanın tamamen kaldırılması** — En temiz çözüm. Center widget'ı zaten layout seviyesinde ortalar. Zoom sonrası kullanıcı istediği gibi pan yapabilir.

5. **translateByDouble / scaleByDouble hatası** — `w` parametresi `1` olmalıydı, `0` kullanılınca transform bozuldu. Eski `translate`/`scale` API'sine geri dönüldü.

6. **_vpSize = Size.zero sıfırlaması (2026-08-03 düzeltildi)** — `_resetView()` ve `_toggleFillFit()` içinde `_vpSize = Size.zero` vardı. Çift tıklama (fill/fit toggle) `setState` yerine yalnızca `_markRenderDirty()` çağırdığı için LayoutBuilder yeniden çalışmıyor, `_vpSize` sıfır kalıyordu → `_handleScroll` ve `_clamp` erken return edip zoom ölüyor, pan sınırsız oluyordu. Çözüm: metot içi sıfırlama kaldırıldı; `_vpSize` yalnızca LayoutBuilder'ın `constraints.biggest`'inden beslenir (alan tanımındaki ilk değer korunur). Linux'ta da mevcuttu, Windows testinde fark edildi.
