changeset 1614:ad9b425f27ae

new class: ArrowSceneLayer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 30 Oct 2020 16:26:39 +0100
parents 5f0660fe06c3
children f5d4bd7b5593
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake OrthancStone/Sources/Scene2D/ArrowSceneLayer.cpp OrthancStone/Sources/Scene2D/ArrowSceneLayer.h OrthancStone/Sources/Scene2D/CairoCompositor.cpp OrthancStone/Sources/Scene2D/ISceneLayer.h OrthancStone/Sources/Scene2D/Internals/CairoArrowRenderer.cpp OrthancStone/Sources/Scene2D/Internals/CairoArrowRenderer.h OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h OrthancStone/Sources/Scene2D/Internals/OpenGLArrowRenderer.cpp OrthancStone/Sources/Scene2D/Internals/OpenGLArrowRenderer.h OrthancStone/Sources/Scene2D/OpenGLCompositor.cpp
diffstat 14 files changed, 556 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 30 11:07:38 2020 +0100
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 30 16:26:39 2020 +0100
@@ -751,7 +751,7 @@
         window.postMessage({
           'type': 'show-osirix-annotations',
           'xml': response.data,
-          'clear': true
+          'clear': false
         }, targetOrigin);
       });
   }
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 30 11:07:38 2020 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -70,6 +70,7 @@
 #include <Messages/ObserverBase.h>
 #include <Oracle/ParseDicomFromWadoCommand.h>
 #include <Oracle/ParseDicomSuccessMessage.h>
+#include <Scene2D/ArrowSceneLayer.h>
 #include <Scene2D/ColorTextureSceneLayer.h>
 #include <Scene2D/FloatTextureSceneLayer.h>
 #include <Scene2D/MacroSceneLayer.h>
@@ -1548,15 +1549,23 @@
                 if (GetCurrentFrameGeometry().ProjectPoint(x1, y1, line.GetPoint1()) &&
                     GetCurrentFrameGeometry().ProjectPoint(x2, y2, line.GetPoint2()))
                 {
-                  std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
-                  OrthancStone::PolylineSceneLayer::Chain chain;
-                  chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
-                  chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
-
-                  // TODO - IsArrow
-                  
-                  layer->AddChain(chain, false, 0, 255, 0);
-                  annotationsLayer->AddLayer(layer.release());
+                  if (line.IsArrow())
+                  {
+                    std::unique_ptr<OrthancStone::ArrowSceneLayer> layer(
+                      new OrthancStone::ArrowSceneLayer(OrthancStone::ScenePoint2D(x1, y1),
+                                                        OrthancStone::ScenePoint2D(x2, y2)));
+                    layer->SetColor(0, 255, 0);
+                    annotationsLayer->AddLayer(layer.release());
+                  }
+                  else
+                  {
+                    std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
+                    OrthancStone::PolylineSceneLayer::Chain chain;
+                    chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
+                    chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
+                    layer->AddChain(chain, false, 0, 255, 0);
+                    annotationsLayer->AddLayer(layer.release());
+                  }
                 }
                 break;
               }
@@ -2303,7 +2312,7 @@
 static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
 static std::string stringBuffer_;
 static bool softwareRendering_ = false;
-static WebViewerAction leftButtonAction_ = WebViewerAction_GrayscaleWindowing;
+static WebViewerAction leftButtonAction_ = WebViewerAction_Rotate;  // WebViewerAction_GrayscaleWindowing;
 static WebViewerAction middleButtonAction_ = WebViewerAction_Pan;
 static WebViewerAction rightButtonAction_ = WebViewerAction_Zoom;
 
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Oct 30 16:26:39 2020 +0100
@@ -255,6 +255,8 @@
   ${ORTHANC_STONE_ROOT}/Oracle/ParseDicomFromFileCommand.cpp
   ${ORTHANC_STONE_ROOT}/Oracle/ParseDicomFromWadoCommand.cpp
 
+  ${ORTHANC_STONE_ROOT}/Scene2D/ArrowSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Scene2D/ArrowSceneLayer.h
   ${ORTHANC_STONE_ROOT}/Scene2D/CairoCompositor.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/CairoCompositor.h
   ${ORTHANC_STONE_ROOT}/Scene2D/Color.h
@@ -297,6 +299,8 @@
   ${ORTHANC_STONE_ROOT}/Scene2D/ZoomSceneTracker.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/ZoomSceneTracker.h
 
+  ${ORTHANC_STONE_ROOT}/Scene2D/Internals/CairoArrowRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Scene2D/Internals/CairoArrowRenderer.h
   ${ORTHANC_STONE_ROOT}/Scene2D/Internals/CairoBaseRenderer.h
   ${ORTHANC_STONE_ROOT}/Scene2D/Internals/CairoColorTextureRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/Internals/CairoColorTextureRenderer.h
@@ -471,6 +475,8 @@
 
     ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h
+    ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLArrowRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLArrowRenderer.h
     ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLBasicPolylineRenderer.h
     ${ORTHANC_STONE_ROOT}/Scene2D/Internals/OpenGLColorTextureProgram.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/ArrowSceneLayer.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -0,0 +1,86 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "ArrowSceneLayer.h"
+
+#include <boost/math/constants/constants.hpp>
+
+namespace OrthancStone
+{
+  ArrowSceneLayer::ArrowSceneLayer(const ScenePoint2D& a,
+                                   const ScenePoint2D& b) :
+    a_(a),
+    b_(b),
+    thickness_(1.0),
+    revision_(0),
+    arrowLength_(10),  // 10 pixels
+    arrowAngle_(boost::math::constants::pi<double>() / 12.0)  // 15 degrees
+  {
+  }
+
+
+  ISceneLayer* ArrowSceneLayer::Clone() const
+  {
+    std::unique_ptr<ArrowSceneLayer> copy(new ArrowSceneLayer(a_, b_));
+    copy->color_ = color_;
+    copy->thickness_ = thickness_;
+    copy->arrowLength_ = arrowLength_;
+    copy->arrowAngle_ = arrowAngle_;
+    return copy.release();
+  }
+    
+
+  void ArrowSceneLayer::SetThickness(double thickness)
+  {
+    thickness_ = thickness;
+    revision_ += 1;
+  }
+
+
+  void ArrowSceneLayer::SetColor(const Color& color)
+  {
+    color_ = color;
+    revision_ += 1;
+  }
+    
+
+  void ArrowSceneLayer::SetArrowLength(double length)
+  {
+    arrowLength_ = length;
+    revision_ += 1;
+  }
+
+
+  void ArrowSceneLayer::SetArrowAngle(double angle)
+  {
+    arrowAngle_ = angle;
+    revision_ += 1;
+  }
+    
+
+  void ArrowSceneLayer::GetBoundingBox(Extent2D& target) const
+  {
+    target.Clear();
+    target.AddPoint(a_.GetX(), a_.GetY());
+    target.AddPoint(b_.GetX(), b_.GetY());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/ArrowSceneLayer.h	Fri Oct 30 16:26:39 2020 +0100
@@ -0,0 +1,107 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "Color.h"
+#include "ScenePoint2D.h"
+#include "ISceneLayer.h"
+
+#include <Compatibility.h>  // For ORTHANC_OVERRIDE
+
+namespace OrthancStone
+{
+  class ArrowSceneLayer : public ISceneLayer
+  {
+  private:
+    ScenePoint2D  a_;
+    ScenePoint2D  b_;
+    Color         color_;
+    double        thickness_;
+    uint64_t      revision_;
+    double        arrowLength_;   // in pixels
+    double        arrowAngle_;    // in radians
+
+  public:
+    ArrowSceneLayer(const ScenePoint2D& a,
+                    const ScenePoint2D& b);
+
+    virtual uint64_t GetRevision() const ORTHANC_OVERRIDE
+    {
+      return revision_;
+    }
+
+    virtual ISceneLayer* Clone() const ORTHANC_OVERRIDE;
+
+    const ScenePoint2D& GetA() const
+    {
+      return a_;
+    }
+
+    const ScenePoint2D& GetB() const
+    {
+      return b_;
+    }
+
+    void SetThickness(double thickness);
+
+    double GetThickness() const
+    {
+      return thickness_;
+    }
+
+    void SetColor(const Color& color);
+    
+    void SetColor(uint8_t red,
+                  uint8_t green,
+                  uint8_t blue)
+    {
+      SetColor(Color(red, green, blue));
+    }
+    
+    const Color& GetColor() const
+    {
+      return color_;
+    }
+
+    void SetArrowLength(double length);
+
+    double GetArrowLength() const
+    {
+      return arrowLength_;
+    }
+
+    void SetArrowAngle(double angle);
+
+    double GetArrowAngle() const
+    {
+      return arrowAngle_;
+    }
+
+    virtual Type GetType() const ORTHANC_OVERRIDE
+    {
+      return Type_Arrow;
+    }
+
+    virtual void GetBoundingBox(Extent2D& target) const ORTHANC_OVERRIDE;
+  };
+}
--- a/OrthancStone/Sources/Scene2D/CairoCompositor.cpp	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Sources/Scene2D/CairoCompositor.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -22,6 +22,7 @@
 
 #include "CairoCompositor.h"
 
+#include "Internals/CairoArrowRenderer.h"
 #include "Internals/CairoColorTextureRenderer.h"
 #include "Internals/CairoFloatTextureRenderer.h"
 #include "Internals/CairoInfoPanelRenderer.h"
@@ -84,6 +85,9 @@
       case ISceneLayer::Type_Macro:
         return new Internals::MacroLayerRenderer(*this, layer);
 
+      case ISceneLayer::Type_Arrow:
+        return new Internals::CairoArrowRenderer(*this, layer);
+
       default:
         return NULL;
     }
--- a/OrthancStone/Sources/Scene2D/ISceneLayer.h	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Sources/Scene2D/ISceneLayer.h	Fri Oct 30 16:26:39 2020 +0100
@@ -41,7 +41,8 @@
       Type_Text,
       Type_FloatTexture,
       Type_LookupTableTexture,
-      Type_Macro
+      Type_Macro,
+      Type_Arrow
     };
 
     virtual ~ISceneLayer()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/Internals/CairoArrowRenderer.cpp	Fri Oct 30 16:26:39 2020 +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-2020 Osimis S.A., 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 "CairoArrowRenderer.h"
+
+#include "../ArrowSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void CairoArrowRenderer::Render(const AffineTransform2D& transform,
+                                    unsigned int canvasWidth,
+                                    unsigned int canvasHeight)
+    {
+      const ArrowSceneLayer& layer = GetLayer<ArrowSceneLayer>();
+      
+      cairo_t* cr = GetCairoContext();
+
+      cairo_set_line_width(cr, layer.GetThickness());
+
+      const Color& color = layer.GetColor();
+      cairo_set_source_rgb(cr, color.GetRedAsFloat(),
+                           color.GetGreenAsFloat(),
+                           color.GetBlueAsFloat());
+
+      const double zoom = transform.ComputeZoom();
+
+      ScenePoint2D axis = layer.GetB() - layer.GetA();
+      double n = ScenePoint2D::SquaredMagnitude(axis);
+      
+      if (!LinearAlgebra::IsCloseToZero(n))
+      {
+        axis = axis * layer.GetArrowLength() / (sqrt(n) * zoom);
+
+        const ScenePoint2D a = layer.GetA().Apply(transform);
+        const ScenePoint2D b = layer.GetB().Apply(transform);
+
+        cairo_move_to(cr, a.GetX(), a.GetY());
+        cairo_line_to(cr, b.GetX(), b.GetY());
+
+        AffineTransform2D r1 = AffineTransform2D::CreateRotation(layer.GetArrowAngle());
+        const ScenePoint2D c = (layer.GetA() + axis.Apply(r1)).Apply(transform);
+        
+        AffineTransform2D r2 = AffineTransform2D::CreateRotation(-layer.GetArrowAngle());
+        const ScenePoint2D d = (layer.GetA() + axis.Apply(r2)).Apply(transform);
+        
+        cairo_move_to(cr, a.GetX(), a.GetY());
+        cairo_line_to(cr, c.GetX(), c.GetY());
+
+        cairo_move_to(cr, a.GetX(), a.GetY());
+        cairo_line_to(cr, d.GetX(), d.GetY());
+
+        cairo_stroke(cr);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/Internals/CairoArrowRenderer.h	Fri Oct 30 16:26:39 2020 +0100
@@ -0,0 +1,45 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "CairoBaseRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoArrowRenderer : public CairoBaseRenderer
+    {
+    public:
+      CairoArrowRenderer(ICairoContextProvider& target,
+                         const ISceneLayer& layer) :
+        CairoBaseRenderer(target, layer)
+      {
+      }
+    
+      virtual void Render(const AffineTransform2D& transform,
+                          unsigned int canvasWidth,
+                          unsigned int canvasHeight) ORTHANC_OVERRIDE;
+    };
+  }
+}
--- a/OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -35,7 +35,7 @@
 
       if (data_.get() == NULL)
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
     }
 
@@ -48,5 +48,16 @@
     {
       LoadLayer(layer);
     }
+
+
+    void OpenGLAdvancedPolylineRenderer::Render(const AffineTransform2D& transform,
+                                                unsigned int canvasWidth,
+                                                unsigned int canvasHeight)
+    {
+      if (!context_.IsContextLost())
+      {
+        program_.Apply(*data_, transform, canvasWidth, canvasHeight, true, true);
+      }
+    }
   }
 }
--- a/OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Sources/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h	Fri Oct 30 16:26:39 2020 +0100
@@ -46,13 +46,7 @@
 
       virtual void Render(const AffineTransform2D& transform,
                           unsigned int canvasWidth,
-                          unsigned int canvasHeight) ORTHANC_OVERRIDE
-      {
-        if (!context_.IsContextLost())
-        {
-          program_.Apply(*data_, transform, canvasWidth, canvasHeight, true, true);
-        }
-      }
+                          unsigned int canvasHeight) ORTHANC_OVERRIDE;
 
       virtual void Update(const ISceneLayer& layer) ORTHANC_OVERRIDE
       {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/Internals/OpenGLArrowRenderer.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -0,0 +1,134 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "OpenGLArrowRenderer.h"
+
+#include <OrthancException.h>
+
+#include <math.h>
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLArrowRenderer::LoadLayer(const ArrowSceneLayer& layer)
+    {
+      // "dataBody_" contains the "body" of the arrow, in scene coordinates
+      {
+        PolylineSceneLayer l;
+        l.SetThickness(layer.GetThickness());
+
+        PolylineSceneLayer::Chain chain;
+        chain.push_back(layer.GetA());
+        chain.push_back(layer.GetB());
+        l.AddChain(chain, false, layer.GetColor());
+
+        dataBody_.reset(new OpenGLLinesProgram::Data(context_, l));
+      }
+
+      // "dataHead_" contains the "head" of the arrow, properly scaled in
+      // pixel coordinates, but with center at (0,0)
+      {
+        PolylineSceneLayer l;
+        l.SetThickness(layer.GetThickness());
+
+        const double c = cos(layer.GetArrowAngle());
+        const double s = sin(layer.GetArrowAngle());
+        
+        PolylineSceneLayer::Chain chain;
+        chain.push_back(ScenePoint2D(c, s) * layer.GetArrowLength());
+        chain.push_back(ScenePoint2D(0, 0));
+        chain.push_back(ScenePoint2D(c, -s) * layer.GetArrowLength());
+        l.AddChain(chain, false, layer.GetColor());
+
+        dataHead_.reset(new OpenGLLinesProgram::Data(context_, l));
+      }
+
+      // Compute a unit vector encoding the direction of the body of the arrow
+      ScenePoint2D direction = layer.GetB() - layer.GetA();
+      double n = ScenePoint2D::SquaredMagnitude(direction);
+      if (LinearAlgebra::IsCloseToZero(n))
+      {
+        direction = ScenePoint2D(1, 0);
+      }
+      else
+      {
+        direction = direction / sqrt(n);
+      }
+
+      // Compute a rotation matrix, to bring the "head" in the axis of the "body"
+      // https://math.stackexchange.com/a/3565068
+      Matrix rotation = LinearAlgebra::ZeroMatrix(3, 3);
+      rotation(0, 0) = direction.GetX();
+      rotation(1, 0) = direction.GetY();
+      rotation(0, 1) = -direction.GetY();
+      rotation(1, 1) = direction.GetX();
+      rotation(2, 2) = 1;
+
+      transformHead_ = AffineTransform2D::Combine(
+        AffineTransform2D::CreateOffset(layer.GetA().GetX(), layer.GetA().GetY()),
+        AffineTransform2D(rotation));
+        
+      if (dataBody_.get() == NULL ||
+          dataHead_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+
+    OpenGLArrowRenderer::OpenGLArrowRenderer(OpenGL::IOpenGLContext& context,
+                                             OpenGLLinesProgram& program,
+                                             const ArrowSceneLayer& layer) :
+      context_(context),
+      program_(program)
+    {
+      LoadLayer(layer);
+    }
+
+
+    void OpenGLArrowRenderer::Render(const AffineTransform2D& transform,
+                                     unsigned int canvasWidth,
+                                     unsigned int canvasHeight)
+    {
+      if (!context_.IsContextLost())
+      {
+        program_.Apply(*dataBody_, transform, canvasWidth, canvasHeight, true, true);
+
+        const double z = 1.0 / transform.ComputeZoom();
+        const AffineTransform2D t2 = AffineTransform2D::Combine(
+          transform,       // 3. Apply the original transform
+          transformHead_,  // 2. Bring the "head" of the arrow at the proper position/angle          
+          AffineTransform2D::CreateScaling(z, z));  // 1. Neutralize the zoom level
+
+        program_.Apply(*dataHead_, t2, canvasWidth, canvasHeight, true, true);
+      }
+    }
+
+
+    void OpenGLArrowRenderer::Update(const ISceneLayer& layer) 
+    {
+      LoadLayer(dynamic_cast<const ArrowSceneLayer&>(layer));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/Internals/OpenGLArrowRenderer.h	Fri Oct 30 16:26:39 2020 +0100
@@ -0,0 +1,57 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "../ArrowSceneLayer.h"
+#include "CompositorHelper.h"
+#include "OpenGLLinesProgram.h"
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLArrowRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&                    context_;
+      OpenGLLinesProgram&                        program_;
+      std::unique_ptr<OpenGLLinesProgram::Data>  dataBody_;
+      std::unique_ptr<OpenGLLinesProgram::Data>  dataHead_;
+      AffineTransform2D                          transformHead_;
+
+      void LoadLayer(const ArrowSceneLayer& layer);
+
+    public:
+      OpenGLArrowRenderer(OpenGL::IOpenGLContext& context,
+                          OpenGLLinesProgram& program,
+                          const ArrowSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform,
+                          unsigned int canvasWidth,
+                          unsigned int canvasHeight) ORTHANC_OVERRIDE;
+
+      virtual void Update(const ISceneLayer& layer) ORTHANC_OVERRIDE;
+    };
+  }
+}
--- a/OrthancStone/Sources/Scene2D/OpenGLCompositor.cpp	Fri Oct 30 11:07:38 2020 +0100
+++ b/OrthancStone/Sources/Scene2D/OpenGLCompositor.cpp	Fri Oct 30 16:26:39 2020 +0100
@@ -22,6 +22,7 @@
 #include "OpenGLCompositor.h"
 
 #include "Internals/OpenGLAdvancedPolylineRenderer.h"
+#include "Internals/OpenGLArrowRenderer.h"
 #include "Internals/OpenGLBasicPolylineRenderer.h"
 #include "Internals/OpenGLColorTextureRenderer.h"
 #include "Internals/OpenGLFloatTextureRenderer.h"
@@ -122,6 +123,10 @@
       case ISceneLayer::Type_Macro:
         return new Internals::MacroLayerRenderer(*this, layer);
 
+      case ISceneLayer::Type_Arrow:
+        return new Internals::OpenGLArrowRenderer
+          (context_, linesProgram_, dynamic_cast<const ArrowSceneLayer&>(layer));
+
       default:
         return NULL;
       }