Extract static DeepZoom rendering system from PR #378#378
Open
mattleibow wants to merge 130 commits intomainfrom
Open
Extract static DeepZoom rendering system from PR #378#378mattleibow wants to merge 130 commits intomainfrom
mattleibow wants to merge 130 commits intomainfrom
Conversation
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>
- 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>
… 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>
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>
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>
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>
…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>
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>
- 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>
… 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>
75187f6 to
a1a33bc
Compare
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Pages/DeepZoom.razor) -- minimal static preview usingSKCanvasViewDemos/DeepZoom/DeepZoomPage) -- minimal static preview usingSKCanvasViewdeep-zoom.md,deep-zoom-blazor.md,deep-zoom-maui.mdWhat's NOT Included (deferred)
SKDeepZoomView)SKGestureTracker)SKAnimationSpring)Architecture
Sample pages pass a collection URI to
SKDeepZoomController, which delegates to tile services. A renderer draws the best-resolution tiles onto a plainSKCanvasView, centered and fit-to-size. The only input is the canvas size.Test Results