chore: release build, .deb package, install.sh release mode
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:
Alhan
2026-07-24 06:26:57 +03:00
parent 4dae4234ba
commit d41c0f0ac2
6 changed files with 279 additions and 3 deletions

Binary file not shown.

View 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 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ü.

View File

@@ -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

Binary file not shown.

Binary file not shown.

45
update_plan_01.md Normal file
View 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
```