chore: release build, .deb package, install.sh release mode
Some checks failed
Build Windows / build-windows (push) Has been cancelled
Some checks failed
Build Windows / build-windows (push) Has been cancelled
- release/imajviewer.deb: 7.3MB .deb paket (dpkg-deb) - release/lib/libapp.so: guncel release binary - deb/usr/lib/imajviewer/lib/libapp.so: guncel - install.sh: release bundle path, release build cmd - docs/zoom-clamp-mekanizmasi.md: teknik dokuman
This commit is contained in:
Binary file not shown.
231
docs/zoom-clamp-mekanizmasi.md
Normal file
231
docs/zoom-clamp-mekanizmasi.md
Normal file
@@ -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ü.
|
||||
@@ -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
|
||||
|
||||
BIN
release/imajviewer.deb
Normal file
BIN
release/imajviewer.deb
Normal file
Binary file not shown.
Binary file not shown.
45
update_plan_01.md
Normal file
45
update_plan_01.md
Normal file
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user