changeset 223:d30a10d574ec am

refactoring continued - not working
author am@osimis.io
date Thu, 14 Jun 2018 10:57:02 +0200
parents 84844649a8fd
children 5fcffbce35a9
files Platforms/WebAssembly/Defaults.cpp Platforms/WebAssembly/Defaults.h Platforms/WebAssembly/stone-framework-loader.ts Platforms/WebAssembly/wasm-application.js Platforms/WebAssembly/wasm-viewport.js
diffstat 5 files changed, 681 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/Platforms/WebAssembly/Defaults.cpp	Tue Jun 12 17:21:15 2018 +0200
+++ b/Platforms/WebAssembly/Defaults.cpp	Thu Jun 14 10:57:02 2018 +0200
@@ -1,2 +1,281 @@
 #include "Defaults.h"
 
+#include "WasmWebService.h"
+#include <Framework/dev.h>
+#include "Framework/Widgets/TestCairoWidget.h"
+#include <Framework/Viewport/WidgetViewport.h>
+#include <Framework/Widgets/LayerWidget.h>
+
+static unsigned int width_ = 0;
+static unsigned int height_ = 0;
+
+/**********************************/
+
+static std::auto_ptr<OrthancStone::BasicWasmApplication> application;
+static std::shared_ptr<OrthancStone::WidgetViewport> viewport_;
+static OrthancStone::ChangeObserver changeObserver_;
+static OrthancStone::StatusBar statusBar_;
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+  using namespace OrthancStone;
+  
+  void EMSCRIPTEN_KEEPALIVE CreateWasmApplication() {
+
+    printf("CreateWasmApplication\n");
+    viewport_.reset(new OrthancStone::WidgetViewport);
+    viewport_->SetStatusBar(statusBar_);
+    viewport_->Register(changeObserver_);
+
+
+    application.reset(CreateUserApplication());
+
+    boost::program_options::options_description options;
+    application->DeclareStartupOptions(options);
+  }
+
+  void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc,
+                                                  const char* value) {
+    application->SetStartupParameter(keyc, value);
+  }
+
+  void EMSCRIPTEN_KEEPALIVE StartWasmApplication() {
+
+    printf("StartWasmApplication\n");
+
+    // recreate a command line from uri arguments and parse it
+    boost::program_options::variables_map parameters;
+    application->GetStartupParameters(parameters);
+
+    BasicWasmApplicationContext& context = dynamic_cast<BasicWasmApplicationContext&>(application->CreateApplicationContext(OrthancStone::WasmWebService::GetInstance(), viewport_));;
+    application->Initialize(statusBar_, parameters);
+
+    viewport_->SetSize(width_, height_);
+    printf("StartWasmApplication - completed\n");
+  }
+
+  // void EMSCRIPTEN_KEEPALIVE ViewportUpdate(const char* _instanceId) {
+  //   printf("updating viewport content, Instance = [%s]\n", instanceId.c_str());
+
+  //   layerSource->LoadFrame(instanceId, 0);
+  //   printf("frame loaded\n");
+  //   instanceWidget->UpdateContent();
+
+  //   printf("update should be done\n");
+  // }
+  
+  // void EMSCRIPTEN_KEEPALIVE ViewportStart()
+  // {
+
+  //   viewport_.reset(new OrthancStone::WidgetViewport);
+  //   viewport_->SetStatusBar(statusBar_);
+  //   viewport_->Register(changeObserver_);
+  //   instanceWidget.reset(new OrthancStone::LayerWidget);
+  //   layerSource = new OrthancStone::OrthancFrameLayerSource(OrthancStone::WasmWebService::GetInstance());
+
+  //   if (!instanceId.empty())
+  //   {
+  //     layerSource->LoadFrame(instanceId, 0);
+  //   } else {
+  //     printf("No instance provided so far\n");
+  //   }
+
+  //   instanceWidget->AddLayer(layerSource);
+
+  //   {
+  //     OrthancStone::RenderStyle s;
+  //     //s.drawGrid_ = true;
+  //     s.alpha_ = 1;
+  //     s.windowing_ = OrthancStone::ImageWindowing_Bone;
+  //     instanceWidget->SetLayerStyle(0, s);
+  //   }
+
+  //   viewport_->SetCentralWidget(instanceWidget.release());
+  //   viewport_->SetSize(width_, height_);
+
+
+  // }
+
+  void EMSCRIPTEN_KEEPALIVE NotifyUpdateContent()
+  {
+    // TODO Only launch the JavaScript timer if "HasUpdateContent()"
+    if (viewport_.get() != NULL &&
+        viewport_->HasUpdateContent())
+    {
+      viewport_->UpdateContent();
+    }
+
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportSetSize(unsigned int width, unsigned int height)
+  {
+    width_ = width;
+    height_ = height;
+    
+    if (viewport_.get() != NULL)
+    {
+      viewport_->SetSize(width, height);
+    }
+  }
+
+  int EMSCRIPTEN_KEEPALIVE ViewportRender(unsigned int width,
+                                          unsigned int height,
+                                          uint8_t* data)
+  {
+    changeObserver_.Reset();
+
+    //printf("ViewportRender called %dx%d\n", width, height);
+    if (width == 0 ||
+        height == 0)
+    {
+      return 1;
+    }
+
+    Orthanc::ImageAccessor surface;
+    surface.AssignWritable(Orthanc::PixelFormat_BGRA32, width, height, 4 * width, data);
+
+    if (viewport_.get() != NULL)
+    {
+      viewport_->Render(surface);
+    }
+
+    // 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.
+    uint8_t* p = data;
+    for (unsigned int y = 0; y < height; y++) {
+      for (unsigned int x = 0; x < width; x++) {
+        uint8_t tmp = p[0];
+        p[0] = p[2];
+        p[2] = tmp;
+        
+        p += 4;
+      }
+    }
+
+    return 1;
+  }
+
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseDown(unsigned int rawButton,
+                                              int x,
+                                              int y,
+                                              unsigned int rawModifiers)
+  {
+    OrthancStone::MouseButton button;
+    switch (rawButton)
+    {
+      case 0:
+        button = OrthancStone::MouseButton_Left;
+        break;
+
+      case 1:
+        button = OrthancStone::MouseButton_Middle;
+        break;
+
+      case 2:
+        button = OrthancStone::MouseButton_Right;
+        break;
+
+      default:
+        return;  // Unknown button
+    }
+
+    if (viewport_.get() != NULL)
+    {
+      viewport_->MouseDown(button, x, y, OrthancStone::KeyboardModifiers_None /* TODO */);
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseWheel(int deltaY,
+                                               int x,
+                                               int y,
+                                               int isControl)
+  {
+    if (viewport_.get() != NULL &&
+        deltaY != 0)
+    {
+      OrthancStone::MouseWheelDirection direction = (deltaY < 0 ?
+                                                     OrthancStone::MouseWheelDirection_Up :
+                                                     OrthancStone::MouseWheelDirection_Down);
+      OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+
+      if (isControl != 0)
+      {
+        modifiers = OrthancStone::KeyboardModifiers_Control;
+      }
+
+      viewport_->MouseWheel(direction, x, y, modifiers);
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseMove(int x,
+                                              int y)
+  {
+    if (viewport_.get() != NULL)
+    {
+      viewport_->MouseMove(x, y);
+    }
+  }
+  
+  void EMSCRIPTEN_KEEPALIVE ViewportKeyPressed(const char* key, 
+                                               bool isShiftPressed, 
+                                               bool isControlPressed,
+                                               bool isAltPressed)
+                                               
+  {
+    if (viewport_.get() != NULL)
+    {
+      OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+      if (isShiftPressed) {
+        modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Shift);
+      }
+      if (isControlPressed) {
+        modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Control);
+      }
+      if (isAltPressed) {
+        modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Alt);
+      }
+      printf("key pressed : %c\n", key[0]);
+      viewport_->KeyPressed(key[0], modifiers);
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseUp()
+  {
+    if (viewport_.get() != NULL)
+    {
+      viewport_->MouseUp();
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseEnter()
+  {
+    if (viewport_.get() != NULL)
+    {
+      viewport_->MouseEnter();
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseLeave()
+  {
+    if (viewport_.get() != NULL)
+    {
+      viewport_->MouseLeave();
+    }
+  }
+
+
+#ifdef __cplusplus
+}
+#endif
--- a/Platforms/WebAssembly/Defaults.h	Tue Jun 12 17:21:15 2018 +0200
+++ b/Platforms/WebAssembly/Defaults.h	Thu Jun 14 10:57:02 2018 +0200
@@ -1,23 +1,35 @@
 #pragma once
 
+#include <emscripten/emscripten.h>
+
 #include <Framework/dev.h>
 #include <Framework/Viewport/WidgetViewport.h>
 #include <Framework/Widgets/LayerWidget.h>
 #include <Framework/Widgets/LayoutWidget.h>
+#include <Applications/Wasm/BasicWasmApplication.h>
+#include <Applications/Wasm/BasicWasmApplicationContext.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
   
+  // JS methods accessible from C++
   extern void ScheduleRedraw();
   
+  // C++ methods accessible from JS
+  extern void EMSCRIPTEN_KEEPALIVE CreateWasmApplication();
+//   extern void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc, const char* value);
+//   extern void EMSCRIPTEN_KEEPALIVE StartWasmApplication();
+
 #ifdef __cplusplus
 }
 #endif
 
+extern OrthancStone::BasicWasmApplication* CreateUserApplication();
 
 namespace OrthancStone {
 
+  // default Ovserver to trigger Viewport redraw when something changes in the Viewport
   class ChangeObserver :
     public OrthancStone::IViewport::IObserver
   {
@@ -46,7 +58,7 @@
     }
   };
 
-
+  // default status bar to log messages on the console/stdout
   class StatusBar : public OrthancStone::IStatusBar
   {
   public:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/WebAssembly/stone-framework-loader.ts	Thu Jun 14 10:57:02 2018 +0200
@@ -0,0 +1,90 @@
+module Stone {
+    /**
+     * This file contains primitives to interface with WebAssembly and
+     * with the Stone framework.
+     **/
+    
+    export declare type InitializationCallback = () => void;
+    
+    export declare var StoneFrameworkModule : any;
+    
+    //const ASSETS_FOLDER : string = "assets/lib";
+    //const WASM_FILENAME : string = "orthanc-framework";
+    
+    
+    export class Framework
+    {
+      private static singleton_ : Framework = null;
+    
+      private constructor(verbose : boolean) 
+      {
+        //this.ccall('Initialize', null, [ 'number' ], [ verbose ]);
+      }
+    
+      
+      public ccall(name: string,
+                   returnType: string,
+                   argTypes: Array<string>,
+                   argValues: Array<any>) : any
+      {
+        return StoneFrameworkModule.ccall(name, returnType, argTypes, argValues);
+      }
+    
+      
+      public cwrap(name: string,
+                   returnType: string,
+                   argTypes: Array<string>) : any
+      {
+        return StoneFrameworkModule.cwrap(name, returnType, argTypes);
+      }
+    
+      
+      public static GetInstance() : Framework
+      {
+        if (Framework.singleton_ == null) {
+          throw new Error('The WebAssembly module is not loaded yet');
+        } else {
+          return Framework.singleton_;
+        }
+      }
+      
+    
+      public static Initialize(verbose: boolean,
+                               callback: InitializationCallback)
+      {
+        console.log('Initializing WebAssembly');
+    
+        (<any> window).StoneFrameworkModule = {
+          preRun: [ 
+            function() {
+              console.log('Loading the Stone Framework using WebAssembly');
+            }
+          ],
+          postRun: [ 
+            function()  {
+              // This function is called by ".js" wrapper once the ".wasm"
+              // WebAssembly module has been loaded and compiled by the
+              // browser
+              console.log('WebAssembly is ready');
+              Framework.singleton_ = new Framework(verbose);
+              callback();
+            }
+          ],
+          print: function(text : string) {
+            console.log(text);
+          },
+          printErr: function(text : string) {
+            console.error(text);
+          },
+          totalDependencies: 0
+        };
+    
+        // Dynamic loading of the JavaScript wrapper around WebAssembly
+        var script = document.createElement('script');
+        script.type = 'application/javascript';
+        script.src = "orthanc-stone.js"; // ASSETS_FOLDER + '/' + WASM_FILENAME + '.js';
+        script.async = true;
+        document.head.appendChild(script);
+      }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/WebAssembly/wasm-application.js	Thu Jun 14 10:57:02 2018 +0200
@@ -0,0 +1,102 @@
+
+
+// Global context used by "library.js"
+var viewport = null;
+var WasmWebService_NotifyError = null;
+var WasmWebService_NotifySuccess = null;
+//var NotifyRestApiGet = null;
+var NotifyUpdateContent = null;
+
+function UpdateContentThread()
+{
+  if (NotifyUpdateContent != null) {
+    NotifyUpdateContent();
+  }
+  
+  setTimeout(UpdateContentThread, 100);  // Update the viewport content every 100ms if need be
+}
+
+
+function GetUriParameters()
+{
+  var parameters = window.location.search.substr(1);
+
+  if (parameters != null &&
+      parameters != '')
+  {
+    var result = {};
+    var tokens = parameters.split('&');
+
+    for (var i = 0; i < tokens.length; i++) {
+      var tmp = tokens[i].split('=');
+      if (tmp.length == 2) {
+        result[tmp[0]] = decodeURIComponent(tmp[1]);
+      }
+    }
+    
+    return result;
+  }
+  else
+  {
+    return {};
+  }
+}
+
+
+
+function InitializeWasmApplication(canvasId)
+{
+  console.log("Initializing wasm-app");
+  viewport = WebAssemblyViewport(StoneFrameworkModule, 'canvas');
+
+  /******************** */
+  SetStartupParameter = StoneFrameworkModule.cwrap('SetStartupParameter', null, [ 'string', 'string' ]);
+  CreateWasmApplication = StoneFrameworkModule.cwrap('CreateWasmApplication', null, [ ], [ ]);
+  StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, [ ], [ ]);
+                                 
+  /******************** */
+
+  // NotifyGlobalParameter = StoneFrameworkModule.cwrap('NotifyGlobalParameter', null,
+  //                                      [ 'string', 'string' ]);
+  // ViewportUpdate = StoneFrameworkModule.cwrap('ViewportUpdate', null,
+  //                                      [ 'string' ]);
+  WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null,
+                                              [ 'number', 'string', 'array', 'number', 'number' ]);
+  WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null,
+                                            [ 'number', 'string', 'number' ]);
+  //NotifyRestApiGet = Module.cwrap('NotifyRestApiGet', null, [ 'number', 'array', 'number' ]);
+  NotifyUpdateContent = StoneFrameworkModule.cwrap('NotifyUpdateContent', null, [  ]);
+
+  // Prevent scrolling
+  document.body.addEventListener('touchmove', function(event) {
+    event.preventDefault();
+  }, false);
+  
+  document.getElementById('canvas').onclick = function() {
+    viewport.Redraw();
+  };
+
+
+  /************************************** */
+  CreateWasmApplication();
+
+  // parse uri and transmit the parameters to the app before initializing it
+  var parameters = GetUriParameters();
+
+  for (var key in parameters) {
+    if (parameters.hasOwnProperty(key)) {  
+      SetStartupParameter(key, parameters[key]);
+    }
+  }
+
+  StartWasmApplication();
+  /************************************** */
+
+  UpdateContentThread();
+}
+
+if (!('WebAssembly' in window)) {
+  alert('Sorry, your browser does not support WebAssembly :(');
+} else {
+  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/WebAssembly/wasm-viewport.js	Thu Jun 14 10:57:02 2018 +0200
@@ -0,0 +1,197 @@
+// http://stackoverflow.com/a/28900478/881731
+
+function WebAssemblyViewport(module, canvasId) {
+    this.module = module;
+    
+    // Obtain a reference to the canvas element using its id
+    this.htmlCanvas = document.getElementById(canvasId);
+  
+    // Obtain a graphics context on the canvas element for drawing.
+    this.context = htmlCanvas.getContext('2d');
+  
+    // ImageData structure that can be used to update the content of the canvas
+    this.imageData = null;
+  
+    // Temporary buffer in the WebAssembly heap that is used to render the pixels
+    this.renderingBuffer = null;
+  
+    // Get access to the WebAssembly functions
+    this.ViewportSetSize = this.module.cwrap('ViewportSetSize', null, [ 'number', 'number' ]);
+    this.ViewportRender = this.module.cwrap('ViewportRender', 'number', [ 'number', 'number', 'number' ]);
+    this.ViewportMouseDown = this.module.cwrap('ViewportMouseDown', null, [ 'number', 'number', 'number', 'number' ]);
+    this.ViewportMouseMove = this.module.cwrap('ViewportMouseMove', null, [ 'number', 'number' ]);
+    this.ViewportMouseUp = this.module.cwrap('ViewportMouseUp', null, [ ]);
+    this.ViewportMouseEnter = this.module.cwrap('ViewportMouseEnter', null, []);
+    this.ViewportMouseLeave = this.module.cwrap('ViewportMouseLeave', null, []);
+    this.ViewportMouseWheel = this.module.cwrap('ViewportMouseWheel', null, [ 'number', 'number', 'number', 'number' ]);
+    this.ViewportKeyPressed = this.module.cwrap('ViewportKeyPressed', null, [ 'string', 'number', 'number' ]);
+  
+    this.Redraw = function() {
+      if (this.imageData === null ||
+          this.renderingBuffer === null ||
+          ViewportRender(this.imageData.width,
+                         this.imageData.height,
+                         this.renderingBuffer) == 0) {
+        console.log('The rendering has failed');
+      } else {
+        // Create an accessor to the rendering buffer (i.e. create a
+        // "window" above the heap of the WASM module), then copy it to
+        // the ImageData object
+        this.imageData.data.set(new Uint8ClampedArray(
+          this.module.buffer,
+          this.renderingBuffer,
+          this.imageData.width * this.imageData.height * 4));
+        
+        this.context.putImageData(imageData, 0, 0);
+      }
+    }
+  
+    this.Resize = function() {
+      if (this.imageData != null &&
+          (this.imageData.width != this.window.innerWidth ||
+           this.imageData.height != this.window.innerHeight)) {
+        this.imageData = null;
+      }
+      
+      this.htmlCanvas.width = window.innerWidth;
+      this.htmlCanvas.height = window.innerHeight;
+  
+      if (this.imageData === null) {
+        this.imageData = context.getImageData(0, 0, this.htmlCanvas.width, this.htmlCanvas.height);
+        ViewportSetSize(this.htmlCanvas.width, this.htmlCanvas.height);
+  
+        if (this.renderingBuffer != null) {
+          this.module._free(this.renderingBuffer);
+        }
+        
+        renderingBuffer = this.module._malloc(this.imageData.width * this.imageData.height * 4);
+      }
+      
+      this.Redraw();
+    }
+  
+    // Force the rendering of the viewport for the first time
+    this.Resize();
+  
+    // Register an event listener to call the Resize() function 
+    // each time the window is resized.
+    window.addEventListener('resize', this.Resize, false);
+  
+    var that = this;
+  
+    this.htmlCanvas.addEventListener('contextmenu', function(event) {
+      // Prevent right click on the canvas
+      event.preventDefault();
+    }, false);
+    
+    this.htmlCanvas.addEventListener('mouseleave', function(event) {
+      that.ViewportMouseLeave();
+    });
+    
+    this.htmlCanvas.addEventListener('mouseenter', function(event) {
+      that.ViewportMouseEnter();
+    });
+  
+    this.htmlCanvas.addEventListener('mousedown', function(event) {
+      var x = event.pageX - this.offsetLeft;
+      var y = event.pageY - this.offsetTop;
+      that.ViewportMouseDown(event.button, x, y, 0 /* TODO */);    
+    });
+  
+    this.htmlCanvas.addEventListener('mousemove', function(event) {
+      var x = event.pageX - this.offsetLeft;
+      var y = event.pageY - this.offsetTop;
+      that.ViewportMouseMove(x, y);
+    });
+  
+    this.htmlCanvas.addEventListener('mouseup', function(event) {
+      that.ViewportMouseUp();
+    });
+  
+    window.addEventListener('keydown', function(event) {
+      that.ViewportKeyPressed(event.key, event.shiftKey, event.ctrlKey, event.altKey);
+    });
+  
+    this.htmlCanvas.addEventListener('wheel', function(event) {
+      var x = event.pageX - this.offsetLeft;
+      var y = event.pageY - this.offsetTop;
+      that.ViewportMouseWheel(event.deltaY, x, y, event.ctrlKey);
+      event.preventDefault();
+    });
+  
+  
+    // Touch events
+    this.touchTranslation = false;
+    this.touchZoom = false;
+  
+    this.ResetTouch = function() {
+      if (this.touchTranslation ||
+          this.touchZoom) {
+        this.ViewportMouseUp();
+      }
+  
+      this.touchTranslation = false;
+      this.touchZoom = false;
+    }
+  
+    this.GetTouchTranslation = function(event) {
+      var touch = event.targetTouches[0];
+      return [
+        touch.pageX,
+        touch.pageY
+      ];
+    }
+    
+    this.GetTouchZoom = function(event) {
+      var touch1 = event.targetTouches[0];
+      var touch2 = event.targetTouches[1];
+      var dx = (touch1.pageX - touch2.pageX);
+      var dy = (touch1.pageY - touch2.pageY);
+      var d = Math.sqrt(dx * dx + dy * dy);
+      return [
+        (touch1.pageX + touch2.pageX) / 2.0,
+        (touch1.pageY + touch2.pageY) / 2.0,
+        d
+      ];
+    }
+    
+    this.htmlCanvas.addEventListener('touchstart', function(event) {
+      ResetTouch();
+    });
+  
+    this.htmlCanvas.addEventListener('touchend', function(event) {
+      ResetTouch();
+    });
+    
+    this.htmlCanvas.addEventListener('touchmove', function(event) {
+      if (that.touchTranslation.length == 2) {
+        var t = GetTouchTranslation(event);
+        that.ViewportMouseMove(t[0], t[1]);
+      }
+      else if (that.touchZoom.length == 3) {
+        var z0 = that.touchZoom;
+        var z1 = GetTouchZoom(event);
+        that.ViewportMouseMove(z0[0], z0[1] - z0[2] + z1[2]);
+      }
+      else {
+        // Realize the gesture event
+        if (event.targetTouches.length == 1) {
+          // Exactly one finger inside the canvas => Setup a translation
+          that.touchTranslation = GetTouchTranslation(event);
+          that.ViewportMouseDown(1 /* middle button */,
+                                 that.touchTranslation[0],
+                                 that.touchTranslation[1], 0);
+        } else if (event.targetTouches.length == 2) {
+          // Exactly 2 fingers inside the canvas => Setup a pinch/zoom
+          that.touchZoom = GetTouchZoom(event);
+          var z0 = that.touchZoom;
+          that.ViewportMouseDown(2 /* right button */,
+                                 z0[0],
+                                 z0[1], 0);
+        }        
+      }
+    });
+    
+    return this;
+  }
+  
\ No newline at end of file