changeset 2240:515bb5c5c8db

added support for pinch/zoom and panning gestures
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 23 May 2025 18:40:26 +0200 (2 weeks ago)
parents 66b79075ac58
children 4cb5be924923
files Applications/StoneWebViewer/NEWS Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.h OrthancStone/Sources/Scene2D/PinchZoomTracker.cpp OrthancStone/Sources/Scene2D/PinchZoomTracker.h OrthancStone/Sources/Toolbox/AffineTransform2D.h OrthancStone/Sources/Viewport/DefaultViewportInteractor.cpp
diffstat 9 files changed, 289 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/NEWS	Sun Apr 27 20:55:48 2025 +0200
+++ b/Applications/StoneWebViewer/NEWS	Fri May 23 18:40:26 2025 +0200
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+* Added support for pinch/zoom and panning gestures
 * Experimental support for DICOM SR, display of textual reports
 * Experimental support for DICOM SR "Measurement Report" (TID 1500 - only polylines)
 * Added "Print" and "Download" buttons in the PDF viewer toolbar
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Sun Apr 27 20:55:48 2025 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri May 23 18:40:26 2025 +0200
@@ -932,6 +932,8 @@
       studies_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Study, filter, tags,
       new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Study));
 
+    pending_++;
+
     // Secondly, load the series
     if (!seriesInstanceUid.empty())
     {
@@ -944,7 +946,7 @@
       series_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Series, filter, tags,
       new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Series));
 
-    pending_ += 2;
+    pending_++;
   }
 
 
@@ -1426,7 +1428,7 @@
         std::string sopClassUid, sopInstanceUid;
         if (accessor.GetInstance(i).LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false) &&
             accessor.GetInstance(i).LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false) &&
-            sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1")
+            OrthancStone::StringToSopClassUid(sopClassUid) == OrthancStone::SopClassUid_EncapsulatedPdf)
         {
           std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
           lock->Schedule(
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Sun Apr 27 20:55:48 2025 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Fri May 23 18:40:26 2025 +0200
@@ -276,6 +276,7 @@
   ${ORTHANC_STONE_ROOT}/Scene2D/MacroSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/MagnifyingGlassTracker.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/PanSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Scene2D/PinchZoomTracker.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/PointerEvent.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/PolylineSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/RotateSceneTracker.cpp
--- a/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.cpp	Sun Apr 27 20:55:48 2025 +0200
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.cpp	Fri May 23 18:40:26 2025 +0200
@@ -330,6 +330,57 @@
   {
   }
 
+  EM_BOOL WebAssemblyViewport::OnTouch(int eventType,
+                                       const EmscriptenTouchEvent *touchEvent,
+                                       void *userData)
+  {
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL &&
+        that->interactor_.get() != NULL)
+    {
+      const ICompositor& compositor = *that->compositor_;
+
+      PointerEvent event;
+      for (int i = 0; i < touchEvent->numTouches; i++)
+      {
+        event.AddPosition(compositor.GetPixelCenterCoordinates(touchEvent->touches[i].targetX, touchEvent->touches[i].targetY));
+      }
+
+      switch (eventType)
+      {
+        case EMSCRIPTEN_EVENT_TOUCHSTART:
+          that->controller_->HandleMousePress(*that->interactor_, event,
+                                              that->compositor_->GetCanvasWidth(),
+                                              that->compositor_->GetCanvasHeight());
+          that->Invalidate();
+          break;
+
+        case EMSCRIPTEN_EVENT_TOUCHMOVE:
+          if (that->controller_->HasActiveTracker() &&
+              that->controller_->HandleMouseMove(event))
+          {
+            that->Invalidate();
+          }
+          break;
+
+        case EMSCRIPTEN_EVENT_TOUCHEND:
+        case EMSCRIPTEN_EVENT_TOUCHCANCEL:
+          if (that->controller_->HasActiveTracker())
+          {
+            that->controller_->HandleMouseRelease(event);
+            that->Invalidate();
+          }
+          break;
+
+        default:
+          break;
+      }
+    }
+
+    return true;
+  }
+
   void WebAssemblyViewport::PostConstructor()
   {
     boost::shared_ptr<IViewport> viewport = shared_from_this();
@@ -385,6 +436,11 @@
                                       reinterpret_cast<void*>(this),
                                       false,
                                       OnMouseUp);
+
+      emscripten_set_touchstart_callback(canvasCssSelector_.c_str(), reinterpret_cast<void*>(this), false, OnTouch);
+      emscripten_set_touchend_callback(canvasCssSelector_.c_str(), reinterpret_cast<void*>(this), false, OnTouch);
+      emscripten_set_touchmove_callback(canvasCssSelector_.c_str(), reinterpret_cast<void*>(this), false, OnTouch);
+      emscripten_set_touchcancel_callback(canvasCssSelector_.c_str(), reinterpret_cast<void*>(this), false, OnTouch);
     }
   }
 
--- a/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.h	Sun Apr 27 20:55:48 2025 +0200
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.h	Fri May 23 18:40:26 2025 +0200
@@ -74,6 +74,8 @@
     
     static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
 
+    static EM_BOOL OnTouch(int eventType, const EmscriptenTouchEvent *mouseEvent, void *userData);
+
   protected:
     void Invalidate();
     
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/PinchZoomTracker.cpp	Fri May 23 18:40:26 2025 +0200
@@ -0,0 +1,127 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PinchZoomTracker.h"
+
+#include "../Scene2DViewport/ViewportController.h"
+#include "../Viewport/ViewportLocker.h"
+
+namespace OrthancStone
+{
+  static ScenePoint2D GetCenter(const PointerEvent& event)
+  {
+    assert(event.GetPositionsCount() == 2);
+    return ScenePoint2D((event.GetPosition(0).GetX() + event.GetPosition(1).GetX()) / 2.0,
+                        (event.GetPosition(0).GetY() + event.GetPosition(1).GetY()) / 2.0);
+  }
+
+
+  PinchZoomTracker::PinchZoomTracker(boost::weak_ptr<IViewport> viewport,
+                                     const PointerEvent& event) :
+    viewport_(viewport),
+    state_(State_Dead)
+  {
+    ViewportLocker locker(viewport_);
+
+    if (locker.IsValid())
+    {
+      originalSceneToCanvas_ = locker.GetController().GetSceneToCanvasTransform();
+      originalCanvasToScene_ = locker.GetController().GetCanvasToSceneTransform();
+
+      if (event.GetPositionsCount() == 1)
+      {
+        state_ = State_OneFinger;
+        pivot_ = event.GetPosition(0).Apply(originalCanvasToScene_);
+        originalDistance_ = 0;
+      }
+      else if (event.GetPositionsCount() == 2)
+      {
+        state_ = State_TwoFingers;
+        pivot_ = GetCenter(event).Apply(originalCanvasToScene_);
+        originalDistance_ = ScenePoint2D::DistancePtPt(event.GetPosition(0), event.GetPosition(1));
+      }
+    }
+  }
+
+
+  void PinchZoomTracker::PointerMove(const PointerEvent &event,
+                                     const Scene2D &scene)
+  {
+    if (state_ == State_OneFinger &&
+        event.GetPositionsCount() == 2)
+    {
+      // Upgrade from 1 finger to 2 fingers, keeping the original pivot point
+      state_ = State_Upgraded;
+      originalDistance_ = ScenePoint2D::DistancePtPt(event.GetPosition(0), event.GetPosition(1));
+    }
+
+    ScenePoint2D p;
+    double zoom;
+
+    if (event.GetPositionsCount() == 1 &&
+        state_ == State_OneFinger)
+    {
+      p = event.GetPosition(0).Apply(originalCanvasToScene_);
+      zoom = 1;
+    }
+    else if (event.GetPositionsCount() == 2)
+    {
+      if (state_ == State_TwoFingers)
+      {
+        p = GetCenter(event).Apply(originalCanvasToScene_);
+      }
+      else if (state_ == State_Upgraded)
+      {
+        p = event.GetPosition(0).Apply(originalCanvasToScene_);
+      }
+      else
+      {
+        state_ = State_Dead;
+        return;
+      }
+
+      double distance = ScenePoint2D::DistancePtPt(event.GetPosition(0), event.GetPosition(1));
+      zoom = distance / originalDistance_;
+    }
+    else
+    {
+      state_ = State_Dead;
+      return;
+    }
+
+    {
+      ViewportLocker locker(viewport_);
+
+      if (locker.IsValid())
+      {
+        locker.GetController().SetSceneToCanvasTransform(
+          AffineTransform2D::Combine(
+            originalSceneToCanvas_,
+            AffineTransform2D::CreateOffset(p.GetX(), p.GetY()),
+            AffineTransform2D::CreateScaling(zoom),
+            AffineTransform2D::CreateOffset(-pivot_.GetX(), -pivot_.GetY())));
+        locker.Invalidate();
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/PinchZoomTracker.h	Fri May 23 18:40:26 2025 +0200
@@ -0,0 +1,79 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../Viewport/IViewport.h"
+#include "PointerEvent.h"
+
+
+namespace OrthancStone
+{
+  class PinchZoomTracker : public IFlexiblePointerTracker
+  {
+  private:
+    enum State
+    {
+      State_OneFinger,
+      State_TwoFingers,
+      State_Upgraded,
+      State_Dead
+    };
+
+    boost::weak_ptr<IViewport> viewport_;
+    State                      state_;
+    AffineTransform2D          originalSceneToCanvas_;
+    AffineTransform2D          originalCanvasToScene_;
+    ScenePoint2D               pivot_;
+    double                     originalDistance_;
+
+  public:
+    PinchZoomTracker(boost::weak_ptr<IViewport> viewport,
+                     const PointerEvent& event);
+
+    virtual void PointerMove(const PointerEvent &event,
+                             const Scene2D &scene) ORTHANC_OVERRIDE;
+
+    virtual void PointerUp(const PointerEvent &event,
+                           const Scene2D &scene) ORTHANC_OVERRIDE
+    {
+      state_ = State_Dead;
+    }
+
+    virtual void PointerDown(const PointerEvent &event,
+                             const Scene2D &scene) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool IsAlive() const ORTHANC_OVERRIDE
+    {
+      return state_ != State_Dead;
+    }
+
+    virtual void Cancel(const Scene2D &scene) ORTHANC_OVERRIDE
+    {
+      state_ = State_Dead;
+    }
+  };
+}
--- a/OrthancStone/Sources/Toolbox/AffineTransform2D.h	Sun Apr 27 20:55:48 2025 +0200
+++ b/OrthancStone/Sources/Toolbox/AffineTransform2D.h	Fri May 23 18:40:26 2025 +0200
@@ -93,6 +93,11 @@
     static AffineTransform2D CreateScaling(double sx,
                                            double sy);
 
+    static AffineTransform2D CreateScaling(double s)
+    {
+      return CreateScaling(s, s);
+    }
+
     static AffineTransform2D CreateRotation(double angle); // CW rotation in radians
 
     static AffineTransform2D CreateRotation(double angle, // CW rotation in radians
--- a/OrthancStone/Sources/Viewport/DefaultViewportInteractor.cpp	Sun Apr 27 20:55:48 2025 +0200
+++ b/OrthancStone/Sources/Viewport/DefaultViewportInteractor.cpp	Fri May 23 18:40:26 2025 +0200
@@ -25,12 +25,15 @@
 #include "../Scene2D/GrayscaleWindowingSceneTracker.h"
 #include "../Scene2D/MagnifyingGlassTracker.h"
 #include "../Scene2D/PanSceneTracker.h"
+#include "../Scene2D/PinchZoomTracker.h"
 #include "../Scene2D/RotateSceneTracker.h"
 #include "../Scene2D/ZoomSceneTracker.h"
 #include "../Scene2DViewport/ViewportController.h"
 
 #include <OrthancException.h>
 
+#include <emscripten.h>
+
 namespace OrthancStone
 {
   IFlexiblePointerTracker* DefaultViewportInteractor::CreateTrackerInternal(
@@ -109,6 +112,17 @@
         action = rightButtonAction_;
         break;
 
+      case MouseButton_None:
+        if (event.GetPositionsCount() == 1 ||
+            event.GetPositionsCount() == 2)
+        {
+          return new PinchZoomTracker(viewport, event);
+        }
+        else
+        {
+          return NULL;
+        }
+
       default:
         return NULL;
     }