diff Framework/Viewport/WebAssemblyViewport.cpp @ 1232:a28861abf888 broker

viewports for WebAssembly
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Dec 2019 17:46:33 +0100
parents 3c9529edf5fd
children a4bb8c2dd211
line wrap: on
line diff
--- a/Framework/Viewport/WebAssemblyViewport.cpp	Mon Dec 09 14:41:37 2019 +0100
+++ b/Framework/Viewport/WebAssemblyViewport.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -21,248 +21,246 @@
 
 #include "WebAssemblyViewport.h"
 
-#include "../StoneException.h"
+#include <Core/OrthancException.h>
 
-#include <emscripten/html5.h>
+#include <boost/make_shared.hpp>
 
 namespace OrthancStone
 {
-  WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas) :
-    WebAssemblyViewport(canvas),
-    context_(canvas)
+  static void ConvertMouseEvent(PointerEvent& target,
+                                const EmscriptenMouseEvent& source,
+                                const ICompositor& compositor)
   {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
-    RegisterContextCallbacks();
-  }
+    int x = static_cast<int>(source.targetX);
+    int y = static_cast<int>(source.targetY);
 
-  WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& scene) :
-    WebAssemblyViewport(canvas, scene),
-    context_(canvas)
-  {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
-    RegisterContextCallbacks();
-  }
-
-  void WebAssemblyOpenGLViewport::UpdateSize()
-  {
-    context_.UpdateSize();  // First read the size of the canvas
-
-    if (compositor_.get() != NULL)
+    switch (source.button)
     {
-      compositor_->Refresh();  // Then refresh the content of the canvas
-    }
-  }
+      case 0:
+        target.SetMouseButton(MouseButton_Left);
+        break;
 
-  /*
-  typedef EM_BOOL (*em_webgl_context_callback)(int eventType, const void *reserved, void *userData);
-
-  EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED
-
-  EMSCRIPTEN_RESULT emscripten_set_webglcontextlost_callback(
-    const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback)
+      case 1:
+        target.SetMouseButton(MouseButton_Middle);
+        break;
 
-  EMSCRIPTEN_RESULT emscripten_set_webglcontextrestored_callback(
-    const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback)
-
-  */
+      case 2:
+        target.SetMouseButton(MouseButton_Right);
+        break;
 
-  EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextLost_callback(
-    int eventType, const void* reserved, void* userData)
-  {
-    ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST);
-    WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData);
-    return viewport->OpenGLContextLost();
+      default:
+        target.SetMouseButton(MouseButton_None);
+        break;
+    }
+      
+    target.AddPosition(compositor.GetPixelCenterCoordinates(x, y));
+    target.SetAltModifier(source.altKey);
+    target.SetControlModifier(source.ctrlKey);
+    target.SetShiftModifier(source.shiftKey);
   }
 
-  EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextRestored_callback(
-    int eventType, const void* reserved, void* userData)
-  {
-    ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED);
-    WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData);
-    return viewport->OpenGLContextRestored();
-  }
 
-  void WebAssemblyOpenGLViewport::DisableCompositor()
-  {
-    compositor_.reset();
-  }
-
-  ICompositor& WebAssemblyOpenGLViewport::GetCompositor()
+  class WebAssemblyViewport::WasmLock : public ILock
   {
-    if (compositor_.get() == NULL)
-    {
-      // "HasCompositor()" should have been called
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return *compositor_;
-    }
-  }
+  private:
+    WebAssemblyViewport& that_;
 
-  void WebAssemblyOpenGLViewport::Refresh()
-  {
-    try
+  public:
+    WasmLock(WebAssemblyViewport& that) :
+      that_(that)
     {
-      if (HasCompositor())
-      {
-        GetCompositor().Refresh();
-      }
-      else
-      {
-        // this block was added because of (perceived?) bugs in the 
-        // browser where the WebGL contexts are NOT automatically restored 
-        // after being lost. 
-        // the WebGL context has been lost. Sce 
-
-        //LOG(ERROR) << "About to call WebAssemblyOpenGLContext::TryRecreate().";
-        //LOG(ERROR) << "Before calling it, isContextLost == " << context_.IsContextLost();
+    }
 
-        if (!context_.IsContextLost())
-        {
-          LOG(TRACE) << "Context restored!";
-          //LOG(ERROR) << "After calling it, isContextLost == " << context_.IsContextLost();
-          RestoreCompositor();
-          UpdateSize();
-        }
-      }
+    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    {
+      return that_.compositor_.get() != NULL;
     }
-    catch (const StoneException& e)
+
+    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
     {
-      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      if (that_.compositor_.get() == NULL)
       {
-        LOG(WARNING) << "Context is lost! Compositor will be disabled.";
-        DisableCompositor();
-        // we now need to wait for the "context restored" callback
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
       }
       else
       {
-        throw;
+        return *that_.compositor_;
       }
     }
-    catch (...)
+
+    virtual ViewportController& GetController() ORTHANC_OVERRIDE
+    {
+      assert(that_.controller_);
+      return *that_.controller_;
+    }
+
+    virtual void Invalidate() ORTHANC_OVERRIDE
     {
-      // something else nasty happened
-      throw;
+      that_.Invalidate();
     }
+  };
+
+
+  EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData)
+  {
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL &&
+        that.controller_ /* should always be true */)
+    {
+      that.Paint(*that.compositor_, *that.controller_);
+    }
+      
+    return true;
   }
 
-  void WebAssemblyOpenGLViewport::RestoreCompositor()
+    
+  EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
   {
-    // the context must have been restored!
-    ORTHANC_ASSERT(!context_.IsContextLost());
-    if (compositor_.get() == NULL)
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL)
     {
-      compositor_.reset(new OpenGLCompositor(context_, GetScene()));
+      that.UpdateSize(*that.compositor_);
+      that.Invalidate();
     }
-    else
-    {
-      LOG(WARNING) << "RestoreCompositor() called for \"" << GetCanvasIdentifier() << "\" while it was NOT lost! Nothing done.";
-    }
+      
+    return true;
   }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextLost()
+
+  EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextLost() for canvas: " << GetCanvasIdentifier();
-    DisableCompositor();
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    LOG(INFO) << "mouse down: " << that.GetFullCanvasId();      
+
+    if (that.compositor_.get() != NULL &&
+        that.interactor_.get() != NULL)
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+
+      that.controller_->HandleMousePress(*that.interactor_, pointer,
+                                         that.compositor_->GetCanvasWidth(),
+                                         that.compositor_->GetCanvasHeight());        
+      that.Invalidate();
+    }
+
+    return true;
+  }
+
+    
+  EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL &&
+        that.controller_->HasActiveTracker())
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+      that.controller_->HandleMouseMove(pointer);        
+      that.Invalidate();
+    }
+
     return true;
   }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextRestored()
+    
+  EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier();
-    
-    // maybe the context has already been restored by other means (the 
-    // Refresh() function)
-    if (!HasCompositor())
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL)
     {
-      RestoreCompositor();
-      UpdateSize();
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+      that.controller_->HandleMouseRelease(pointer);
+      that.Invalidate();
     }
-    return false;
+
+    return true;
   }
 
-  void WebAssemblyOpenGLViewport::RegisterContextCallbacks()
+    
+  void WebAssemblyViewport::Invalidate()
   {
-#if 0
-    // DISABLED ON 2019-08-20 and replaced by external JS calls because I could
-    // not get emscripten API to work
-    // TODO: what's the impact of userCapture=true ?
-    const char* canvasId = GetCanvasIdentifier().c_str();
-    void* that = reinterpret_cast<void*>(this);
-    EMSCRIPTEN_RESULT status = EMSCRIPTEN_RESULT_SUCCESS;
+    emscripten_request_animation_frame(OnRequestAnimationFrame, this);
+  }
+    
 
-    //status = emscripten_set_webglcontextlost_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextLost_callback);
-    //if (status != EMSCRIPTEN_RESULT_SUCCESS)
-    //{
-    //  std::stringstream ss;
-    //  ss << "Error while calling emscripten_set_webglcontextlost_callback for: \"" << GetCanvasIdentifier() << "\"";
-    //  std::string msg = ss.str();
-    //  LOG(ERROR) << msg;
-    //  ORTHANC_ASSERT(false, msg.c_str());
-    //}
-
-    status = emscripten_set_webglcontextrestored_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextRestored_callback);
-    if (status != EMSCRIPTEN_RESULT_SUCCESS)
+  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
+  {
+    if (compositor == NULL)
     {
-      std::stringstream ss;
-      ss << "Error while calling emscripten_set_webglcontextrestored_callback for: \"" << GetCanvasIdentifier() << "\"";
-      std::string msg = ss.str();
-      LOG(ERROR) << msg;
-      ORTHANC_ASSERT(false, msg.c_str());
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
-    LOG(TRACE) << "WebAssemblyOpenGLViewport::RegisterContextCallbacks() SUCCESS!!!";
-#endif
+    else
+    {
+      compositor_.reset(compositor);
+    }
   }
 
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas) :
-    WebAssemblyViewport(canvas),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
+
+  WebAssemblyViewport::WebAssemblyViewport(const std::string& canvasId,
+                                           const Scene2D* scene) :
+    shortCanvasId_(canvasId),
+    fullCanvasId_("#" + canvasId),
+    interactor_(new DefaultViewportInteractor)
   {
-  }
+    if (scene == NULL)
+    {
+      controller_ = boost::make_shared<ViewportController>();
+    }
+    else
+    {
+      controller_ = boost::make_shared<ViewportController>(*scene);
+    }
+
+    LOG(INFO) << "Initializing Stone viewport on HTML canvas: " << canvasId;
 
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& scene) :
-    WebAssemblyViewport(canvas, scene),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
-  {
+    if (canvasId.empty() ||
+        canvasId[0] == '#')
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "The canvas identifier must not start with '#'");
+    }
+
+    // Disable right-click on the canvas (i.e. context menu)
+    EM_ASM({
+        document.getElementById(UTF8ToString($0)).oncontextmenu = function(event) {
+          event.preventDefault();
+        }
+      },
+      canvasId.c_str()   // $0
+      );
+
+    // It is not possible to monitor the resizing of individual
+    // canvas, so we track the full window of the browser
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnResize);
+
+    emscripten_set_mousedown_callback(fullCanvasId_.c_str(), this, false, OnMouseDown);
+    emscripten_set_mousemove_callback(fullCanvasId_.c_str(), this, false, OnMouseMove);
+    emscripten_set_mouseup_callback(fullCanvasId_.c_str(), this, false, OnMouseUp);
   }
 
-  void WebAssemblyCairoViewport::UpdateSize()
+  
+  IViewport::ILock* WebAssemblyViewport::Lock()
   {
-    LOG(INFO) << "updating cairo viewport size";
-    double w, h;
-    emscripten_get_element_css_size(canvas_.c_str(), &w, &h);
-
-    /**
-     * Emscripten has the function emscripten_get_element_css_size()
-     * to query the width and height of a named HTML element. I'm
-     * calling this first to get the initial size of the canvas DOM
-     * element, and then call emscripten_set_canvas_size() to
-     * initialize the framebuffer size of the canvas to the same
-     * size as its DOM element.
-     * https://floooh.github.io/2017/02/22/emsc-html.html
-     **/
-    unsigned int canvasWidth = 0;
-    unsigned int canvasHeight = 0;
-
-    if (w > 0 ||
-      h > 0)
-    {
-      canvasWidth = static_cast<unsigned int>(boost::math::iround(w));
-      canvasHeight = static_cast<unsigned int>(boost::math::iround(h));
-    }
-
-    emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth, canvasHeight);
-    compositor_.UpdateSize(canvasWidth, canvasHeight);
+    return new WasmLock(*this);
   }
 
-  void WebAssemblyCairoViewport::Refresh()
+  
+  void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor)
   {
-    LOG(INFO) << "refreshing cairo viewport, TODO: blit to the canvans.getContext('2d')";
-    GetCompositor().Refresh();
+    if (interactor == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      interactor_.reset(interactor);
+    }
   }
 }