# HG changeset patch # User Sebastien Jodogne # Date 1575909993 -3600 # Node ID a28861abf8887ac9b07cbe2aac295ea358685d6c # Parent b9f2a111c5b9ff70b9cde2730fa682e6781b1d29 viewports for WebAssembly diff -r b9f2a111c5b9 -r a28861abf888 Framework/Scene2DViewport/ViewportController.cpp --- a/Framework/Scene2DViewport/ViewportController.cpp Mon Dec 09 14:41:37 2019 +0100 +++ b/Framework/Scene2DViewport/ViewportController.cpp Mon Dec 09 17:46:33 2019 +0100 @@ -66,13 +66,22 @@ ViewportController::ViewportController() : undoStackW_(boost::make_shared()), - canvasToSceneFactor_(1) + canvasToSceneFactor_(1), + scene_(new Scene2D) { } - ViewportController::ViewportController(boost::weak_ptr undoStackW) - : undoStackW_(undoStackW) - , canvasToSceneFactor_(1) + ViewportController::ViewportController(const Scene2D& scene) : + undoStackW_(boost::make_shared()), + canvasToSceneFactor_(1), + scene_(scene.Clone()) + { + } + + ViewportController::ViewportController(boost::weak_ptr undoStackW) : + undoStackW_(undoStackW), + canvasToSceneFactor_(1), + scene_(new Scene2D) { } @@ -171,27 +180,27 @@ OrthancStone::AffineTransform2D ViewportController::GetCanvasToSceneTransform() const { - return scene_.GetCanvasToSceneTransform(); + return scene_->GetCanvasToSceneTransform(); } OrthancStone::AffineTransform2D ViewportController::GetSceneToCanvasTransform() const { - return scene_.GetSceneToCanvasTransform(); + return scene_->GetSceneToCanvasTransform(); } void ViewportController::SetSceneToCanvasTransform(const AffineTransform2D& transform) { - scene_.SetSceneToCanvasTransform(transform); + scene_->SetSceneToCanvasTransform(transform); - canvasToSceneFactor_ = scene_.GetCanvasToSceneTransform().ComputeZoom(); + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); BroadcastMessage(SceneTransformChanged(*this)); } void ViewportController::FitContent(unsigned int viewportWidth, unsigned int viewportHeight) { - scene_.FitContent(viewportWidth, viewportHeight); - canvasToSceneFactor_ = scene_.GetCanvasToSceneTransform().ComputeZoom(); + scene_->FitContent(viewportWidth, viewportHeight); + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); BroadcastMessage(SceneTransformChanged(*this)); } diff -r b9f2a111c5b9 -r a28861abf888 Framework/Scene2DViewport/ViewportController.h --- a/Framework/Scene2DViewport/ViewportController.h Mon Dec 09 14:41:37 2019 +0100 +++ b/Framework/Scene2DViewport/ViewportController.h Mon Dec 09 17:46:33 2019 +0100 @@ -111,6 +111,8 @@ ViewportController(); + ViewportController(const Scene2D& scene /* will be cloned */); + ViewportController(boost::weak_ptr undoStackW); ~ViewportController(); @@ -210,12 +212,12 @@ const Scene2D& GetScene() const { - return scene_; + return *scene_; } Scene2D& GetScene() { - return scene_; + return *scene_; } bool HasActiveTracker() const @@ -230,7 +232,7 @@ std::vector > measureTools_; boost::shared_ptr activeTracker_; // TODO - Couldn't this be a "std::auto_ptr"? - Scene2D scene_; + std::auto_ptr scene_; // this is cached double canvasToSceneFactor_; diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebAssemblyCairoViewport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.cpp Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,133 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebAssemblyCairoViewport.h" + +#include "../Scene2D/CairoCompositor.h" + +#include + +namespace OrthancStone +{ + void WebAssemblyCairoViewport::GetCanvasSize(unsigned int& width, + unsigned int& height) + { + double w, h; + emscripten_get_element_css_size(GetFullCanvasId().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 + **/ + if (w > 0 && + h > 0) + { + width = static_cast(boost::math::iround(w)); + height = static_cast(boost::math::iround(h)); + } + else + { + width = 0; + height = 0; + } + } + + + void WebAssemblyCairoViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + compositor.Refresh(controller.GetScene()); + + // Create a temporary memory buffer for the canvas in JavaScript + Orthanc::ImageAccessor cairo; + dynamic_cast(compositor).GetCanvas().GetReadOnlyAccessor(cairo); + + const unsigned int width = cairo.GetWidth(); + const unsigned int height = cairo.GetHeight(); + + if (javascript_.get() == NULL || + javascript_->GetWidth() != width || + javascript_->GetHeight() != height) + { + javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, + true /* force minimal pitch */)); + } + + // Convert from BGRA32 memory layout (only color mode supported + // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32 + // (as expected by HTML5 canvas). This simply amounts to + // swapping the B and R channels. Alpha channel is also set to + // full opacity (255). + uint8_t* q = reinterpret_cast(javascript_->GetBuffer()); + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(cairo.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; // R + q[1] = p[1]; // G + q[2] = p[0]; // B + q[3] = 255; // A + + p += 4; + q += 4; + } + } + + // Execute JavaScript commands to blit the image buffer onto the + // 2D drawing context of the HTML5 canvas + EM_ASM({ + const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3); + const img = new ImageData(data, $2, $3); + const ctx = document.getElementById(UTF8ToString($0)).getContext('2d'); + ctx.putImageData(img, 0, 0); + }, + GetShortCanvasId().c_str(), // $0 + javascript_->GetBuffer(), // $1 + javascript_->GetWidth(), // $2 + javascript_->GetHeight()); // $3 + } + + + void WebAssemblyCairoViewport::UpdateSize(ICompositor& compositor) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height); + + dynamic_cast(compositor).UpdateSize(width, height); + } + + + WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvasId) : + WebAssemblyViewport(canvasId, NULL) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height); + AcquireCompositor(new CairoCompositor(width, height)); + } +} diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebAssemblyCairoViewport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.h Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebAssemblyViewport.h" + +namespace OrthancStone +{ + class WebAssemblyCairoViewport : public WebAssemblyViewport + { + private: + std::auto_ptr javascript_; + + void GetCanvasSize(unsigned int& width, + unsigned int& height); + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + WebAssemblyCairoViewport(const std::string& canvasId); + + virtual ~WebAssemblyCairoViewport() + { + ClearCompositor(); + } + }; +} diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebAssemblyViewport.cpp --- 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 -#include +#include 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(source.targetX); + int y = static_cast(source.targetY); - WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr& 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(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(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(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(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(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(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(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(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(); + } + else + { + controller_ = boost::make_shared(*scene); + } + + LOG(INFO) << "Initializing Stone viewport on HTML canvas: " << canvasId; - WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr& 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(boost::math::iround(w)); - canvasHeight = static_cast(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); + } } } diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebAssemblyViewport.h --- a/Framework/Viewport/WebAssemblyViewport.h Mon Dec 09 14:41:37 2019 +0100 +++ b/Framework/Viewport/WebAssemblyViewport.h Mon Dec 09 17:46:33 2019 +0100 @@ -13,7 +13,7 @@ * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . **/ @@ -21,106 +21,78 @@ #pragma once -#include "../OpenGL/WebAssemblyOpenGLContext.h" -#include "../Scene2D/OpenGLCompositor.h" -#include "../Scene2D/CairoCompositor.h" -#include "ViewportBase.h" +#if !defined(ORTHANC_ENABLE_WASM) +# error Macro ORTHANC_ENABLE_WASM must be defined +#endif + +#if ORTHANC_ENABLE_WASM != 1 +# error This file can only be used if targeting WebAssembly +#endif + +#include "IViewport.h" + +#include +#include namespace OrthancStone { - class WebAssemblyViewport : public ViewportBase + class WebAssemblyViewport : public IViewport { private: - std::string canvasIdentifier_; + class WasmLock; + + std::string shortCanvasId_; + std::string fullCanvasId_; + std::auto_ptr compositor_; + boost::shared_ptr controller_; + std::auto_ptr interactor_; + + static EM_BOOL OnRequestAnimationFrame(double time, void *userData); + + static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData); - public: - WebAssemblyViewport(const std::string& canvasIdentifier) : - canvasIdentifier_(canvasIdentifier) + static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + protected: + void Invalidate(); + + void ClearCompositor() { + compositor_.reset(); } - WebAssemblyViewport(const std::string& canvasIdentifier, - boost::shared_ptr& scene) : - ViewportBase(scene), - canvasIdentifier_(canvasIdentifier) + bool HasCompositor() const { + return compositor_.get() != NULL; } - const std::string& GetCanvasIdentifier() const - { - return canvasIdentifier_; - } - }; + void AcquireCompositor(ICompositor* compositor /* takes ownership */); + virtual void Paint(ICompositor& compositor, + ViewportController& controller) = 0; - class WebAssemblyOpenGLViewport : public WebAssemblyViewport - { - private: - OpenGL::WebAssemblyOpenGLContext context_; - std::auto_ptr compositor_; + virtual void UpdateSize(ICompositor& compositor) = 0; public: - WebAssemblyOpenGLViewport(const std::string& canvas); - - WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr& scene); - - // This function must be called each time the browser window is resized - void UpdateSize(); + WebAssemblyViewport(const std::string& canvasId, + const Scene2D* scene); + + virtual ILock* Lock() ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE + void AcquireInteractor(IViewportInteractor* interactor); + + const std::string& GetShortCanvasId() const { - return (compositor_.get() != NULL); - } - - bool IsContextLost() - { - return context_.IsContextLost(); + return shortCanvasId_; } - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE; - - virtual void Refresh() ORTHANC_OVERRIDE; - - // this does NOT return whether the context is lost! This is called to - // tell Stone that the context has been lost - bool OpenGLContextLost(); - - // This should be called to indicate that the context has been lost - bool OpenGLContextRestored(); - - private: - void DisableCompositor(); - void RestoreCompositor(); - - void RegisterContextCallbacks(); - }; - - - class WebAssemblyCairoViewport : public WebAssemblyViewport - { - private: - CairoCompositor compositor_; - std::string canvas_; - - public: - WebAssemblyCairoViewport(const std::string& canvas); - - WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr& scene); - - void UpdateSize(); - - virtual void Refresh() ORTHANC_OVERRIDE; - - virtual bool HasCompositor() const ORTHANC_OVERRIDE + const std::string& GetFullCanvasId() const { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return compositor_; + return fullCanvasId_; } }; } diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebGLViewport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.cpp Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,105 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebGLViewport.h" + +#include "../StoneException.h" +#include "../Scene2D/OpenGLCompositor.h" + +namespace OrthancStone +{ + void WebGLViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + try + { + compositor.Refresh(controller.GetScene()); + + /** + * No need to manually swap the buffer: "Rendered WebGL content + * is implicitly presented (displayed to the user) on the canvas + * when the event handler that renders with WebGL returns back + * to the browser event loop." + * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context + * + * Could call "emscripten_webgl_commit_frame()" if + * "explicitSwapControl" option were set to "true". + **/ + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + + void WebGLViewport::UpdateSize(ICompositor& compositor) + { + try + { + context_.UpdateSize(); + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + + WebGLViewport::WebGLViewport(const std::string& canvasId) : + WebAssemblyViewport(canvasId, NULL), + context_(GetFullCanvasId()) + { + AcquireCompositor(new OpenGLCompositor(context_)); + } + + + WebGLViewport::WebGLViewport(const std::string& canvasId, + const Scene2D& scene) : + WebAssemblyViewport(canvasId, &scene), + context_(GetFullCanvasId()) + { + AcquireCompositor(new OpenGLCompositor(context_)); + } + + + WebGLViewport::~WebGLViewport() + { + // Make sure to delete the compositor before its parent "context_" gets deleted + ClearCompositor(); + } +} diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebGLViewport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.h Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,53 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebAssemblyViewport.h" +#include "../OpenGL/WebAssemblyOpenGLContext.h" + +namespace OrthancStone +{ + class WebGLViewport : public WebAssemblyViewport + { + private: + OpenGL::WebAssemblyOpenGLContext context_; + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + WebGLViewport(const std::string& canvasId); + + WebGLViewport(const std::string& canvasId, + const Scene2D& scene); + + virtual ~WebGLViewport(); + + bool IsContextLost() + { + return context_.IsContextLost(); + } + }; +} diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebGLViewportsRegistry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.cpp Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,173 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebGLViewportsRegistry.h" + +#include + +namespace OrthancStone +{ + void WebGLViewportsRegistry::LaunchTimer() + { + emscripten_set_timeout(OnTimeoutCallback, 1000.0 * static_cast(timeoutSeconds_), this); + } + + + void WebGLViewportsRegistry::OnTimeout() + { + for (Viewports::iterator it = viewports_.begin(); it != viewports_.end(); ++it) + { + if (it->second == NULL || + it->second->IsContextLost()) + { + LOG(INFO) << "WebGL context lost for canvas: " << it->first; + + // Try and duplicate the HTML5 canvas in the DOM + EM_ASM({ + var canvas = document.getElementById(UTF8ToString($0)); + if (canvas) { + var parent = canvas.parentElement; + if (parent) { + var cloned = canvas.cloneNode(true /* deep copy */); + parent.insertBefore(cloned, canvas); + parent.removeChild(canvas); + } + } + }, + it->first.c_str() // $0 = ID of the canvas + ); + + // At this point, the old canvas is removed from the DOM and + // replaced by a fresh one with the same ID: Recreate the + // WebGL context on the new canvas + std::auto_ptr viewport; + + { + std::auto_ptr lock(it->second->Lock()); + viewport.reset(new WebGLViewport(it->first, lock->GetController().GetScene())); + } + + // Replace the old WebGL viewport by the new one + delete it->second; + it->second = viewport.release(); + + // Tag the fresh canvas as needing a repaint + { + std::auto_ptr lock(it->second->Lock()); + lock->Invalidate(); + } + } + } + + LaunchTimer(); + } + + + void WebGLViewportsRegistry::OnTimeoutCallback(void *userData) + { + WebGLViewportsRegistry& that = *reinterpret_cast(userData); + that.OnTimeout(); + } + + + WebGLViewportsRegistry::WebGLViewportsRegistry(unsigned int timeoutSeconds) : + timeoutSeconds_(timeoutSeconds) + { + if (timeoutSeconds <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + LaunchTimer(); + } + + + WebGLViewportsRegistry::~WebGLViewportsRegistry() + { + for (Viewports::iterator it = viewports_.begin(); it != viewports_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + } + + + void WebGLViewportsRegistry::Add(const std::string& canvasId) + { + if (viewports_.find(canvasId) != viewports_.end()) + { + LOG(ERROR) << "Canvas was already registered: " << canvasId; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + viewports_[canvasId] = new WebGLViewport(canvasId); + } + } + + + void WebGLViewportsRegistry::Remove(const std::string& canvasId) + { + Viewports::iterator found = viewports_.find(canvasId); + + if (found == viewports_.end()) + { + LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId; + } + else + { + if (found->second != NULL) + { + delete found->second; + } + + viewports_.erase(found); + } + } + + + WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId) : + that_(that) + { + Viewports::iterator viewport = that.viewports_.find(canvasId); + if (viewport != that.viewports_.end() && + viewport->second != NULL) + { + lock_.reset(viewport->second->Lock()); + } + } + + + IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const + { + if (IsValid()) + { + return *lock_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r b9f2a111c5b9 -r a28861abf888 Framework/Viewport/WebGLViewportsRegistry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.h Mon Dec 09 17:46:33 2019 +0100 @@ -0,0 +1,77 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebGLViewport.h" + +namespace OrthancStone +{ + /** + * This singleton class must be used if many WebGL viewports are + * created by the higher-level application, implying possible loss + * of WebGL contexts. The object will run an infinite update loop + * that checks whether all the WebGL context are still valid (not + * lost). If some WebGL context is lost, it is automatically + * reinitialized by created a fresh HTML5 canvas. + **/ + class WebGLViewportsRegistry : public boost::noncopyable + { + private: + typedef std::map Viewports; + + unsigned int timeoutSeconds_; + Viewports viewports_; + + void LaunchTimer(); + + void OnTimeout(); + + static void OnTimeoutCallback(void *userData); + + public: + WebGLViewportsRegistry(unsigned int timeoutSeconds); + + ~WebGLViewportsRegistry(); + + void Add(const std::string& canvasId); + + void Remove(const std::string& canvasId); + + class Accessor : public boost::noncopyable + { + private: + WebGLViewportsRegistry& that_; + std::auto_ptr lock_; + + public: + Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId); + + bool IsValid() const + { + return lock_.get() != NULL; + } + + IViewport::ILock& GetViewport() const; + }; + }; +} diff -r b9f2a111c5b9 -r a28861abf888 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Mon Dec 09 14:41:37 2019 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Mon Dec 09 17:46:33 2019 +0100 @@ -445,6 +445,9 @@ if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES ${ORTHANC_STONE_ROOT}/Framework/Oracle/WebAssemblyOracle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyCairoViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h ) endif() @@ -694,10 +697,10 @@ if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.h - ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewportsRegistry.cpp ) endif() endif()