Skip to content

Extract static DeepZoom rendering system from PR #378#378

Open
mattleibow wants to merge 130 commits intomainfrom
feature/deep-zoom-on-gestures
Open

Extract static DeepZoom rendering system from PR #378#378
mattleibow wants to merge 130 commits intomainfrom
feature/deep-zoom-on-gestures

Conversation

@mattleibow
Copy link
Collaborator

@mattleibow mattleibow commented Mar 6, 2026

Summary

Extracts the minimal static DeepZoom rendering system -- just the core services needed to load and render Deep Zoom collections without gestures, animations, or custom controls.

What's Included

  • DeepZoom services: Collection loading, image metadata, tile loaders/fetchers
  • 454 DeepZoom tests -- all passing
  • Blazor sample page (Pages/DeepZoom.razor) -- minimal static preview using SKCanvasView
  • MAUI sample page (Demos/DeepZoom/DeepZoomPage) -- minimal static preview using SKCanvasView
  • Documentation -- deep-zoom.md, deep-zoom-blazor.md, deep-zoom-maui.md

What's NOT Included (deferred)

  • No custom controls (SKDeepZoomView)
  • No gesture system (SKGestureTracker)
  • No animations (SKAnimationSpring)
  • No user interaction beyond view size

Architecture

Sample pages pass a collection URI to SKDeepZoomController, which delegates to tile services. A renderer draws the best-resolution tiles onto a plain SKCanvasView, centered and fit-to-size. The only input is the canvas size.

Test Results

  • 911 tests pass (454 DeepZoom + 457 Extended)
  • Blazor sample: 0 errors
  • MAUI sample: 0 errors

Copilot AI and others added 30 commits February 5, 2026 02:39
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
…y structs, remove selection tracking

Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Fixes identified by Claude Opus, GPT-5.1, and Gemini 3 Pro reviews:

SKGestureEngine (High priority):
- Fix double-tap spatial check: was always 0 (distance compared to self)
- Prevent tap firing after pan if finger returns near start point
- Fix multi-touch overwriting _initialTouch and clearing fling tracker
- Use touch-down time for tap duration instead of last move timestamp

SKGestureEngine (Medium priority):
- Fire DragEnded on touch cancel (was leaving consumers in drag state)
- Ensure DragStarted always precedes DragUpdated/Ended (pinch→pan)
- Restrict pinch/rotate to exactly 2 touches (3+ was undefined)

SKGestureEngine (Low priority):
- Guard long press timer callback against stale execution
- Smooth pinch-to-pan transition with DragStarted on finger lift

GestureHelpers:
- Increase FlingTracker MaxSize from 2 to 5 for reliable velocity
- Use weighted average velocity calculation favoring recent events

GesturePage demo:
- Fix HitTest to account for canvas transforms (pan/zoom/rotate)

Tests: 40 tests passing (6 new tests for bug fixes)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The base class SKSurfaceView pre-scales the canvas by display density,
so touch coordinates (in pixels) need to be divided by the scale factor
to match the point-based canvas coordinate system. Also fix SKImageInfo
to report point-based dimensions instead of pixel dimensions.

In the demo, skip canvas panning when a sticker is selected to prevent
double-movement (both pan and drag firing simultaneously).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Double-tap zooms 2x toward the tapped location, keeping the content
under the tap point fixed. When already at max zoom (3x), double-tap
resets the view to 1x centered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move DrawGrid call inside the save/restore transform block so the
checkerboard background pans, zooms, and rotates together with the
stickers. Expand grid coverage dynamically based on zoom level.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Grid: use index-based (row+col)%2 alternation instead of coordinate
  division which broke with negative values due to C# truncation
- Transform: rotate/scale around view center instead of a drifting
  content point that moved with pan offset
- Double-tap zoom: update pivot math for new transform order
- HitTest: match new transform order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add AdjustOffsetForPivot helper that computes the offset correction
needed so the content under the touch center stays fixed when scale
or rotation changes. Used by OnPinch, OnRotate, and OnDoubleTap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pan delta from the gesture engine is in screen coordinates but the
canvas offset lives after the rotation/scale transform. Inverse-rotate
and inverse-scale the delta before applying it so panning left always
moves content left regardless of canvas rotation. Same fix for fling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add PreviousCenter to SKRotateEventArgs (matching SKPinchEventArgs).
In both OnPinch and OnRotate, apply the center delta as a pan so
content follows the midpoint of the two fingers while pinching or
rotating. The pinch center always stays under the pinch center.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OnPinch and OnRotate both fire on the same touch move event. Both
were applying the center movement as a pan delta, doubling it. Now
only OnPinch applies the center pan.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Toolbar gear icon opens a settings page with:
- Toggle switches for each gesture type (tap, double-tap, long press,
  pan, pinch, rotate, fling, drag)
- Touch slop slider (1-50px)
- Long press duration slider (100-2000ms)
- Current transform state display
- Reset view button

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HitTest was using PostConcat which built the matrix in the wrong order
compared to the canvas pre-concat calls. Rebuilt using PreConcat to
match. Also gate the center-tracking pan inside OnPinch on _enablePan
so disabling pan prevents pinch-induced panning too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PreConcat(B) means this = this · B. To reproduce the canvas CTM,
start from the first canvas call and PreConcat each subsequent one
in the same order. Previous code started from the last call which
reversed the matrix multiplication order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 30 new tests covering all gesture scenarios and edge cases:
- Tap duration: mouse click timeout, long hold with micro-moves
- Pinch data: center/previousCenter, scale direction, zoom-out
- Rotate data: previousCenter, center tracking, zero-rotation
- 3+ touches: no crash, resume pinch after lifting to 2 fingers
- Drag lifecycle: delta accuracy, event ordering, pinch-to-pan
- Fling edge cases: pause before release, vertical, diagonal
- Cancel: during pinch, during detecting (no drag events)
- Sequential gestures: pan-then-tap, pinch-then-tap, 5 taps
- Dispose/reset: mid-gesture dispose, reset allows new gesture
- Edge values: zero delta, zero radius, duplicate touch ID
- Double-tap: too slow (> 300ms delay)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename GestureState → SKGestureState
- Rename FlingTracker → SKFlingTracker
- Rename TouchState → SKTouchState
- Rename PinchState → SKPinchState
- Split GestureHelpers.cs into individual type files
- Split SKGestureEventArgs.cs into 8 individual files
- Remove GestureState enum from SKGestureEngine.cs
- Update all references in engine and tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add internal Timer-based fling animation loop to engine
- Add Flinging event (fires each frame with velocity + delta)
- Add FlingCompleted event (fires when velocity drops below minimum)
- Add FlingFriction, FlingMinVelocity, FlingFrameInterval properties
- Add IsFlinging property and StopFling() method
- Auto-stop fling on new touch down
- Extend SKFlingEventArgs with DeltaX, DeltaY, Speed
- Wire new events through SKGestureSurfaceView
- Simplify sample: remove timer/animation logic from GesturePage
- Add 11 new fling animation tests (81 total)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix hover: handle inContact=false before _touches check so hover
  works without a prior ProcessTouchDown (mouse move on macOS)
- Add ProcessMouseWheel to SKGestureEngine
- Add ScrollDetected event with SKScrollEventArgs
- Handle WheelChanged, Entered, Exited in SKGestureSurfaceView
- Add scroll-to-zoom in sample (zooms at mouse position)
- Add 'Scroll Zoom' toggle to settings
- 6 new tests (87 total): hover without touch down, scroll events

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Constructor is SpringAnimator(double initialValue = 0.0), not named params
- Target is a property, not a SetTarget() method
- Expanded Properties section to document all key SpringAnimator properties

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 7, 2026
- deep-zoom.md: Core concepts, architecture, shared classes (DeepZoomController, DziTileSource, Viewport, TileCache, tile fetchers)
- deep-zoom-maui.md: SKDeepZoomView properties, events, methods, keyboard nav, gesture customisation
- deep-zoom-blazor.md: Full Blazor integration guide with coordinate translation, pointer wiring, SKGestureView
- toc.yml: Point MAUI and Blazor sections to their own pages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 7, 2026
… one type per file

- Moved source/SkiaSharp.Extended.DeepZoom/ into source/SkiaSharp.Extended/DeepZoom/
- Renamed all public types to SKDeepZoom* prefix:
  - DeepZoomController → SKDeepZoomController
  - DeepZoomRenderer → SKDeepZoomRenderer
  - DeepZoomSubImage → SKDeepZoomSubImage
  - DisplayRect → SKDeepZoomDisplayRect
  - DzcSubImage → SKDeepZoomCollectionSubImage
  - DzcTileSource → SKDeepZoomCollectionSource
  - DziTileSource → SKDeepZoomImageSource
  - TileCache → SKDeepZoomTileCache
  - TileId → SKDeepZoomTileId
  - ITileFetcher → ISKDeepZoomTileFetcher
  - HttpTileFetcher → SKDeepZoomHttpTileFetcher
  - FileTileFetcher → SKDeepZoomFileTileFetcher
  - TileScheduler → SKDeepZoomTileScheduler
  - TileRequest → SKDeepZoomTileRequest
  - Viewport → SKDeepZoomViewport
  - ViewportState → SKDeepZoomViewportState
  - TileFailedEventArgs → SKDeepZoomTileFailedEventArgs
- Split multi-type files (one class/struct/interface per file)
- Moved ViewportSpring from Animation/ to DeepZoom/ with namespace SkiaSharp.Extended.DeepZoom
- Removed 'no MAUI dependency' comments
- Updated MAUI DeepZoom view, Blazor sample, test project, solution, and docs
- Removed standalone SkiaSharp.Extended.DeepZoom project from solution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 11, 2026
mattleibow and others added 2 commits March 12, 2026 17:27
Replace old DziTileSource with SKDeepZoomImageSource and
ITileFetcher with ISKDeepZoomTileFetcher following the refactoring
that merged the DeepZoom project into SkiaSharp.Extended.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Zoom in Blazor sample

DeepZoom was merged into the core SkiaSharp.Extended project.
The Blazor sample only needs the single SkiaSharp.Extended reference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 12, 2026
github-actions bot pushed a commit that referenced this pull request Mar 12, 2026
The CI runs .NET 10 SDK which doesn't provide the net9.0 apphost
on Windows, causing MSB3030 errors. Match the other test projects
by targeting net10.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 12, 2026
The test was using real wall-clock time (Task.Delay) to drive touch
events, making velocity calculation unreliable on loaded CI machines.
Under load, the 20ms delays could expand to 200ms+, causing fling
tracker events to fall outside the velocity window and report zero.

Fix: use CreateTracker() with fake TimeProvider (like all sibling
fling tests) and advance fake time by 16ms on each FlingUpdated
callback. With FlingFriction=0.5 this halves velocity each frame,
completing in ~7 frames regardless of wall-clock scheduling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 12, 2026
mattleibow and others added 2 commits March 13, 2026 03:43
…ring and SKGestureView, add Blazor SKDeepZoomView component

- Rename SKTimerAnimation → SKAnimationTimer (file + class)
- Rename SKEasingFunctions → SKAnimationEasing (file + class)
- Rename SpringAnimator → SKAnimationSpring (file + class)
- Remove ViewportSpring (unused in production code) and its 15 tests
- Remove SKGestureView MAUI control (unused)
- Create proper Blazor SKDeepZoomView component in samples/SkiaSharpDemo.Blazor/Components/
  encapsulating SKGestureTracker + SKDeepZoomController with pointer event handling
- Simplify DeepZoom.razor page to use the new SKDeepZoomView component
- Update all references across source, tests, samples, and docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… dead EventCallbacks in Blazor SKDeepZoomView

- ISKDeepZoomTileSource doesn't exist; the concrete type is SKDeepZoomImageSource
- Update TileSource parameter and Load() method in SKDeepZoomView.razor
- Update _tileSource field type in DeepZoom.razor
- Remove OnImageLoaded/OnImageError EventCallbacks (were declared but never invoked)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
Previously the initial viewport was set to ViewportWidth=1.0 (fill/crop
mode), which cropped tall images when the control is wider in aspect
ratio than the image.

Changes:
- Add SKDeepZoomViewport.FitToView(): computes the ViewportWidth that
  shows the entire image centered within the control, sets MaxViewportWidth
  to the fit value so the user cannot zoom out further than fit
- SKDeepZoomController.Load() and ResetView() now call FitToView()
  instead of hardcoding ViewportWidth=1.0 / origin=(0,0)
- Blazor SKDeepZoomView: on ImageOpenSucceeded, sync tracker FROM the
  viewport (fit state) instead of resetting tracker to scale=1 (fill mode)
- MAUI SKDeepZoomView: same ImageOpenSucceeded fix; ResetView() now calls
  controller.ResetView() + SyncTrackerFromViewport() instead of tracker.Reset()
- Lower MinScale from 1f to 0.001f in both deep zoom views; zoom-out
  floor is now enforced by viewport MaxViewportWidth via Constrain()
- Add 3 tests for FitToView() covering wide images, tall images, and
  the invariant that the full image is visible after FitToView()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
- FitToView() else branch (wide images): compute vpOriginY as
  (imageLogicalHeight - vpHeight) / 2 so wide images are centered
  vertically (previously set vpOriginY=0, leaving image at top)
- Constrain() <= 1.0 branch: when vpHeight >= imageLogicalHeight,
  center vertically instead of clamping to 0 (same pattern as the
  > 1.0 branch that centers horizontally for tall images)
- Update 3 DeepZoomControllerTest tests to expect fit-mode values
  (512x512 in 800x600 → fitWidth=800/600, originX=(1-fitWidth)/2)
  rather than the old fill values (width=1.0, originX=0.0)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
… and gestures

- Remove SKDeepZoomView MAUI library and component (no custom controls)
- Remove Blazor SKDeepZoomView component with gesture code
- Rewrite MAUI DeepZoom sample page to use SKCanvasView + SKDeepZoomController directly
- Rewrite Blazor DeepZoom sample page to use SKCanvasView + SKDeepZoomController directly
- Remove SKAnimationSpringTest (animation is not part of the extracted system)
- Rewrite all three deep-zoom docs to document the minimal static architecture
  (no gestures, no custom views, fit-and-center rendering, controller-driven)

The only input to the rendering pipeline is canvas size. The controller handles
tile loading, scheduling, caching, and rendering. Images are centered and scaled
to fit the canvas (scale-to-fit, no cropping or distortion).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow changed the title Add Deep Zoom support using unified SKGestureTracker (rebased on gestures PR) Extract static DeepZoom rendering system from PR #378 Mar 13, 2026
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
@mattleibow mattleibow force-pushed the feature/deep-zoom-on-gestures branch from 75187f6 to a1a33bc Compare March 13, 2026 03:28
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
…rors

- Remove obsolete FilterQuality from SKPaint in SKDeepZoomRenderer
- Remove unused _fadePaint field
- Wrap Load() methods in try/catch to properly raise ImageOpenFailed event

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Mar 13, 2026
mattleibow added a commit that referenced this pull request Mar 13, 2026
Extract the core DeepZoom rendering system from PR #378:
- DeepZoom collection/image source parsers (DZC/DZI XML)
- Tile cache, scheduler, and HTTP/file fetchers
- Controller (orchestrates loading), viewport (centered-fit geometry), renderer (draws tiles)
- 436 unit tests (animation tests excluded — no dependency on gestures/animations)
- Blazor sample page (Deep Zoom Preview) with testgrid DZI static asset
- MAUI sample page (Deep Zoom Preview) with testgrid DZI app-package asset
- Documentation: deep-zoom.md, deep-zoom-blazor.md, deep-zoom-maui.md
- Solution file updated to include DeepZoom test project
- DeepZoom added to MAUI ExtendedDemos and Blazor NavMenu

No gestures, animations, or custom controls included.
This is a minimal static preview system for gigapixel images.
Images render centered-fit (aspect ratio preserved, no cropping/stretching).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants