Files
imajviewer/docs/zoom-clamp-mekanizmasi.md
git_alhan 65c78c4c38 fix: cift tiklamada zoom/pan olumu (_vpSize sifirlamasi kaldirildi), windows setup.exe guncellendi
_toggleFillFit/_resetView icinde _vpSize = Size.zero, LayoutBuilder
yeniden calismadiginda zoom'u olduruyor ve pan'i sinirsiz yapiyordu.
_vpSize artik yalnizca constraints.biggest'ten beslenir.
2026-08-03 11:06:12 +03:00

234 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 111154)
```
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 5071)
```
_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 7580)
```
_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 84107)
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 3945)
```
_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.