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

8.7 KiB
Raw Blame History

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

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)
// 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)
_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()
_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)

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 okumaframeBuilder 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.