changeset 1815:b81775f1b196

New tools for annotations: segment length, circle, angle and eraser
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 25 May 2021 18:07:52 +0200
parents 53f3411bf94b
children dccdc7e59929
files Applications/StoneWebViewer/NEWS Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 4 files changed, 154 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/NEWS	Tue May 25 15:52:38 2021 +0200
+++ b/Applications/StoneWebViewer/NEWS	Tue May 25 18:07:52 2021 +0200
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+* New tools for annotations: segment length, circle, angle and eraser
 * Patient birth date is now displayed in the overlays
 * New argument "token" to set HTTP header "Authorization: Bearer <token>"
   for each request to the DICOMweb server
--- a/Applications/StoneWebViewer/WebApplication/app.js	Tue May 25 15:52:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Tue May 25 18:07:52 2021 +0200
@@ -391,6 +391,8 @@
       globalConfiguration: {},
       creatingArchive: false,
       archiveJob: '',
+      annotationsCurrentAction: stone.WebViewerAction.NONE,  // dummy value
+      annotationsBackupAction: stone.WebViewerAction.NONE,  // dummy value
 
       modalWarning: false,
       modalNotDiagnostic: false,
@@ -652,6 +654,8 @@
           sopInstanceUid: info.sopInstanceUid
         };
       }
+
+      this.ResetAnnotationsAction();
     },
     
     ClickSeries: function(seriesIndex) {
@@ -749,6 +753,7 @@
       }
 
       this.FitContent();
+      this.ResetAnnotationsAction();
     },
 
     UpdateSeriesThumbnail: function(seriesInstanceUid) {
@@ -860,9 +865,37 @@
 
     SetMouseButtonActions: function(left, middle, right) {
       this.mouseActionsVisible = false;
+      this.annotationsBackupAction = stone.WebViewerAction.NONE;
       stone.SetMouseButtonActions(left, middle, right);
     },
 
+    SetAnnotationsAction: function(action) {
+      if (this.annotationsCurrentAction == action) {
+        this.ResetAnnotationsAction();
+      } else {
+        this.annotationsCurrentAction = action;
+
+        if (this.annotationsBackupAction == stone.WebViewerAction.NONE) {
+          this.annotationsBackupAction = stone.GetLeftMouseButtonAction();
+        }
+
+        stone.SetMouseButtonActions(action,
+                                    stone.GetMiddleMouseButtonAction(),
+                                    stone.GetRightMouseButtonAction());
+      }
+    },
+
+    ResetAnnotationsAction: function() {
+      if (this.annotationsBackupAction != stone.WebViewerAction.NONE) {
+        stone.SetMouseButtonActions(this.annotationsBackupAction,
+                                    stone.GetMiddleMouseButtonAction(),
+                                    stone.GetRightMouseButtonAction());
+      }
+
+      this.annotationsBackupAction = stone.WebViewerAction.NONE;
+      this.annotationsCurrentAction = stone.WebViewerAction.NONE;
+    },    
+
     LoadOsiriXAnnotations: function(xml, clearPrevious)
     {
       if (stone.LoadOsiriXAnnotations(xml, clearPrevious)) {
@@ -1037,6 +1070,14 @@
       var seriesInstanceUid = args.detail.seriesInstanceUid;
       that.UpdateIsSeriesComplete(studyInstanceUid, seriesInstanceUid);
     });
+
+    window.addEventListener('StoneAnnotationAdded', function() {
+      that.ResetAnnotationsAction();
+    });
+
+    window.addEventListener('StoneAnnotationRemoved', function() {
+      that.ResetAnnotationsAction();
+    });
   }
 });
 
@@ -1144,7 +1185,6 @@
 
 
 
-
 function ParseJsonWithComments(json)
 {
   if (typeof(json) == 'string') {
--- a/Applications/StoneWebViewer/WebApplication/index.html	Tue May 25 15:52:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Tue May 25 18:07:52 2021 +0200
@@ -472,6 +472,8 @@
             
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      v-bind:class="{ active: (annotationsCurrentAction == stone.WebViewerAction.CREATE_SEGMENT) }"
+                      v-on:click="SetAnnotationsAction(stone.WebViewerAction.CREATE_SEGMENT)"
                       data-toggle="tooltip" data-title="Measure length">
                 <i class="fas fa-arrows-alt-h"></i>
               </button>
@@ -479,6 +481,8 @@
 
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      v-bind:class="{ active: (annotationsCurrentAction == stone.WebViewerAction.CREATE_ANGLE) }" 
+                      v-on:click="SetAnnotationsAction(stone.WebViewerAction.CREATE_ANGLE)"
                       data-toggle="tooltip" data-title="Measure angle">
                 <i class="fas fa-angle-left fa-lg"></i>
               </button>
@@ -486,6 +490,8 @@
 
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      v-bind:class="{ active: (annotationsCurrentAction == stone.WebViewerAction.CREATE_CIRCLE) }" 
+                      v-on:click="SetAnnotationsAction(stone.WebViewerAction.CREATE_CIRCLE)"
                       data-toggle="tooltip" data-title="Measure circle">
                 <i class="far fa-circle"></i>
               </button>
@@ -493,6 +499,15 @@
 
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      v-bind:class="{ active: (annotationsCurrentAction == stone.WebViewerAction.REMOVE_MEASURE) }" 
+                      v-on:click="SetAnnotationsAction(stone.WebViewerAction.REMOVE_MEASURE)"
+                      data-toggle="tooltip" data-title="Delete measurement">
+                <i class="fas fa-trash"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
                       data-toggle="tooltip" data-title="Synchronized browsing"
                       v-bind:class="{ 'active' : synchronizedBrowsing }"
                       v-on:click="synchronizedBrowsing = !synchronizedBrowsing">
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Tue May 25 15:52:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Tue May 25 18:07:52 2021 +0200
@@ -130,7 +130,9 @@
 
 enum STONE_WEB_VIEWER_EXPORT WebViewerAction
 {
-  WebViewerAction_Windowing,
+  WebViewerAction_None,
+    
+    WebViewerAction_Windowing,
     WebViewerAction_Zoom,
     WebViewerAction_Pan,
     WebViewerAction_Rotate,
@@ -139,7 +141,7 @@
     WebViewerAction_CreateAngle,
     WebViewerAction_CreateCircle,
     WebViewerAction_CreateSegment,
-    WebViewerAction_DeleteMeasure
+    WebViewerAction_RemoveMeasure
     };
   
 
@@ -160,11 +162,12 @@
     case WebViewerAction_Rotate:
       return OrthancStone::MouseAction_Rotate;
       
+    case WebViewerAction_None:
     case WebViewerAction_Crosshair:
     case WebViewerAction_CreateAngle:
     case WebViewerAction_CreateCircle:
     case WebViewerAction_CreateSegment:
-    case WebViewerAction_DeleteMeasure:
+    case WebViewerAction_RemoveMeasure:
       return OrthancStone::MouseAction_None;
 
     default:
@@ -1323,6 +1326,10 @@
     virtual void SignalStoneAnnotationsChanged(const ViewerViewport& viewport,
                                                const std::string& sopInstanceUid,
                                                size_t frame) = 0;
+
+    virtual void SignalStoneAnnotationAdded(const ViewerViewport& viewport) = 0;
+
+    virtual void SignalStoneAnnotationRemoved(const ViewerViewport& viewport) = 0;
   };
 
 private:
@@ -2287,11 +2294,21 @@
   void Handle(const OrthancStone::AnnotationsSceneLayer::AnnotationAddedMessage& message)
   {
     RefreshAnnotations(true /* save */);
+
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalStoneAnnotationAdded(*this);
+    }
   }
 
   void Handle(const OrthancStone::AnnotationsSceneLayer::AnnotationRemovedMessage& message)
   {
     RefreshAnnotations(true /* save */);
+
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalStoneAnnotationRemoved(*this);
+    }
   }
 
 public:
@@ -2771,15 +2788,42 @@
       }
       else
       {
+        // Only the left mouse button can be used to edit/create/remove annotations
+        if (event.GetMouseButton() == OrthancStone::MouseButton_Left)
         {
-          std::unique_ptr<OrthancStone::IViewport::ILock> lock2(lock1->Lock());
-
-          std::unique_ptr<OrthancStone::IFlexiblePointerTracker> t;
-          t.reset(viewer_.stoneAnnotations_->CreateTracker(event.GetMainPosition(), lock2->GetController().GetScene()));
-
-          if (t.get() != NULL)
+          switch (leftAction_)
           {
-            return t.release();        
+            case WebViewerAction_CreateAngle:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Angle);
+              break;
+              
+            case WebViewerAction_CreateCircle:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Circle);
+              break;
+              
+            case WebViewerAction_CreateSegment:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Segment);
+              break;
+
+            case WebViewerAction_RemoveMeasure:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Remove);
+              break;
+
+            default:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
+              break;
+          }
+
+          {
+            std::unique_ptr<OrthancStone::IViewport::ILock> lock2(lock1->Lock());
+
+            std::unique_ptr<OrthancStone::IFlexiblePointerTracker> t;
+            t.reset(viewer_.stoneAnnotations_->CreateTracker(event.GetMainPosition(), lock2->GetController().GetScene()));
+
+            if (t.get() != NULL)
+            {
+              return t.release();
+            }
           }
         }
 
@@ -3166,6 +3210,28 @@
       }
     }
   }
+  
+  virtual void SignalStoneAnnotationAdded(const ViewerViewport& viewport) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("StoneAnnotationAdded", false, false,
+                                    { "canvasId" : UTF8ToString($0) });
+        window.dispatchEvent(customEvent);
+      },
+      viewport.GetCanvasId().c_str());
+  }
+
+  virtual void SignalStoneAnnotationRemoved(const ViewerViewport& viewport) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("StoneAnnotationRemoved", false, false,
+                                    { "canvasId" : UTF8ToString($0) });
+        window.dispatchEvent(customEvent);
+      },
+      viewport.GetCanvasId().c_str());
+  }
 };
 
 
@@ -3699,6 +3765,27 @@
 
 
   EMSCRIPTEN_KEEPALIVE
+  int GetLeftMouseButtonAction()
+  {
+    return static_cast<int>(leftButtonAction_);
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  int GetMiddleMouseButtonAction()
+  {
+    return static_cast<int>(middleButtonAction_);
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  int GetRightMouseButtonAction()
+  {
+    return static_cast<int>(rightButtonAction_);
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
   void FitForPrint()
   {
     try