diff --git a/deb/usr/lib/imajviewer/lib/libapp.so b/deb/usr/lib/imajviewer/lib/libapp.so index ad01187..b62456e 100644 Binary files a/deb/usr/lib/imajviewer/lib/libapp.so and b/deb/usr/lib/imajviewer/lib/libapp.so differ diff --git a/docs/zoom-clamp-mekanizmasi.md b/docs/zoom-clamp-mekanizmasi.md new file mode 100644 index 0000000..3bfcc99 --- /dev/null +++ b/docs/zoom-clamp-mekanizmasi.md @@ -0,0 +1,231 @@ +# 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ü. diff --git a/install.sh b/install.sh index 48ce3a2..9229ab0 100755 --- a/install.sh +++ b/install.sh @@ -3,7 +3,7 @@ set -euo pipefail APP=imajviewer SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BUNDLE_DIR="${SCRIPT_DIR}/build/linux/x64/debug/bundle" +BUNDLE_DIR="${SCRIPT_DIR}/build/linux/x64/release/bundle" INSTALL_DIR="${HOME}/.local/lib/${APP}" BIN_LINK="${HOME}/.local/bin/${APP}" DESKTOP_FILE="${HOME}/.local/share/applications/${APP}.desktop" @@ -29,8 +29,8 @@ if [ ! -f "$BUNDLE_DIR/${APP}" ]; then else info "Binary bulunamadı, Flutter build başlatılıyor…" cd "$SCRIPT_DIR" - flutter build linux --debug 2>&1 | tail -1 || err "Flutter build başarısız! Sistemde Flutter SDK yoksa önce git clone yapıp './install.sh' çalıştırın." - BUNDLE_DIR="${SCRIPT_DIR}/build/linux/x64/debug/bundle" + flutter build linux 2>&1 | tail -1 || err "Flutter build başarısız! Sistemde Flutter SDK yoksa önce git clone yapıp './install.sh' çalıştırın." + BUNDLE_DIR="${SCRIPT_DIR}/build/linux/x64/release/bundle" [ -f "$BUNDLE_DIR/${APP}" ] || err "Build başarısız!" fi fi diff --git a/release/imajviewer.deb b/release/imajviewer.deb new file mode 100644 index 0000000..b208ee2 Binary files /dev/null and b/release/imajviewer.deb differ diff --git a/release/lib/libapp.so b/release/lib/libapp.so index ad01187..b62456e 100644 Binary files a/release/lib/libapp.so and b/release/lib/libapp.so differ diff --git a/update_plan_01.md b/update_plan_01.md new file mode 100644 index 0000000..fea06e6 --- /dev/null +++ b/update_plan_01.md @@ -0,0 +1,45 @@ +# ImajViewer — Pan Sınırlama & Zoom Merkezi Sorunu ✅ ÇÖZÜLDÜ + +> **Tarih:** 2026-07-23 +> **Dosya:** `lib/widgets/image_canvas.dart` +> **Durum:** ✅ Çözüldü + +--- + +## Nihai Çözüm + +### 1. Zoom — `Matrix4.copy(m)..translate..scale..translate` +Cursor merkezli zoomAroundPoint. Hem fill hem fit modunda çalışır çünkü `localPosition` her zaman Listener'a göre doğru koordinat verir. `_currentScale` listener'da güncellenir. + +### 2. `_clampAfterZoom` — Image offset + sadece boundary clamp +**Kritik:** Fit modunda (BoxFit.contain) Image, Center widget içinde **ortalanır**. Offset hesaplanarak image'ın viewport'taki gerçek sınırları bulunur: + +```dart +imgOffsetX = (viewportW - imageW) / 2 // fill modunda 0, fit modunda >0 +imgLeft = tx + imgOffsetX * scale +imgRight = tx + (imgOffsetX + imageW) * scale +``` + +**SADECE** image viewport'tan büyükse clamp yapılır. Küçükse **dokunulmaz** — zoom-around-point merkezi korunur. Kullanıcı double-tap ile istediği zaman ortalamayı sıfırlayabilir (`Matrix4.identity()`). + +### 3. `translate` — asla `setTranslationRaw` kullanma +`setTranslationRaw` matrisin translation sütununu ezer → zoom merkezini bozar. `translate(dx,dy)` ekleme yapar → zoom merkezi korunur. + +### 4. Listener'da clamp YOK +Sadece `_currentScale` takip edilir. Pan → `boundaryMargin: EdgeInsets.zero`, Zoom → `_handleScroll` içinde `_clampAfterZoom`. + +--- + +## Akış + +``` +Scroll → _handleScroll → zoomAroundPoint → _controller.value + ↓ + _clampAfterZoom() + ├─ image > viewport → translate ile clamp + └─ image ≤ viewport → dokunma + +Pan → InteractiveViewer → _controller.value (boundaryMargin sınırlar) + ↓ + _onTransformChanged → sadece _currentScale +```