view Platforms/Wasm/wasm-viewport.ts @ 920:5ca418d6579e refactor-viewport-controller

Close branch refactor-viewport-controller.
author Alain Mazy <am@osimis.io>
date Fri, 19 Jul 2019 14:15:49 +0000
parents d5579bdc59b5
children 67d0a8da4afe
line wrap: on
line source

import wasmApplicationRunner = require('./wasm-application-runner');
import * as Logger from './logger'

var isPendingRedraw = false;

function ScheduleWebViewportRedraw(cppViewportHandle: any) : void
{
  if (!isPendingRedraw) {
    isPendingRedraw = true;
    Logger.defaultLogger.debug('Scheduling a refresh of the viewport, as its content changed');
    window.requestAnimationFrame(function() {
      isPendingRedraw = false;
      WasmViewport.GetFromCppViewport(cppViewportHandle).Redraw();
    });
  }
}

(<any>window).ScheduleWebViewportRedraw = ScheduleWebViewportRedraw;

declare function UTF8ToString(any): string;

function CreateWasmViewport(htmlCanvasId: string) : any {
  var cppViewportHandle = wasmApplicationRunner.CreateCppViewport();
  var canvasId = UTF8ToString(htmlCanvasId);
  var webViewport = new WasmViewport((<any> window).StoneFrameworkModule, canvasId, cppViewportHandle);  // viewports are stored in a static map in WasmViewport -> won't be deleted
  webViewport.Initialize();

  return cppViewportHandle;
}
 
(<any>window).CreateWasmViewport = CreateWasmViewport;

export class WasmViewport {

    private static viewportsMapByCppHandle_ : Map<number, WasmViewport> = new Map<number, WasmViewport>(); // key = the C++ handle
    private static viewportsMapByCanvasId_ : Map<string, WasmViewport> = new Map<string, WasmViewport>(); // key = the canvasId

    private module_ : any;
    private canvasId_ : string;
    private htmlCanvas_ : HTMLCanvasElement;
    private context_ : CanvasRenderingContext2D;
    private imageData_ : any = null;
    private renderingBuffer_ : any = null;
    
    private touchGestureInProgress_: boolean = false;
    private touchCount_: number = 0;
    private touchGestureLastCoordinates_: [number, number][] = []; // last x,y coordinates of each touch
    
    private touchZoom_ : any = false;
    private touchTranslation_ : any = false;

    private ViewportSetSize : Function;
    private ViewportRender : Function;
    private ViewportMouseDown : Function;
    private ViewportMouseMove : Function;
    private ViewportMouseUp : Function;
    private ViewportMouseEnter : Function;
    private ViewportMouseLeave : Function;
    private ViewportMouseWheel : Function;
    private ViewportKeyPressed : Function;
    private ViewportTouchStart : Function;
    private ViewportTouchMove : Function;
    private ViewportTouchEnd : Function;

    private pimpl_ : any; // Private pointer to the underlying WebAssembly C++ object

    public constructor(module: any, canvasId: string, cppViewport: any) {
      
      this.pimpl_ = cppViewport;
      WasmViewport.viewportsMapByCppHandle_[this.pimpl_] = this;
      WasmViewport.viewportsMapByCanvasId_[canvasId] = this;

      this.module_ = module;
      this.canvasId_ = canvasId;
      this.htmlCanvas_ = document.getElementById(this.canvasId_) as HTMLCanvasElement;
      if (this.htmlCanvas_ == null) {
        Logger.defaultLogger.error("Can not create WasmViewport, did not find the canvas whose id is '", this.canvasId_, "'");
      }
      this.context_ = this.htmlCanvas_.getContext('2d');

      this.ViewportSetSize = this.module_.cwrap('ViewportSetSize', null, [ 'number', 'number', 'number' ]);
      this.ViewportRender = this.module_.cwrap('ViewportRender', null, [ 'number', 'number', 'number', 'number' ]);
      this.ViewportMouseDown = this.module_.cwrap('ViewportMouseDown', null, [ 'number', 'number', 'number', 'number', 'number' ]);
      this.ViewportMouseMove = this.module_.cwrap('ViewportMouseMove', null, [ 'number', 'number', 'number' ]);
      this.ViewportMouseUp = this.module_.cwrap('ViewportMouseUp', null, [ 'number' ]);
      this.ViewportMouseEnter = this.module_.cwrap('ViewportMouseEnter', null, [ 'number' ]);
      this.ViewportMouseLeave = this.module_.cwrap('ViewportMouseLeave', null, [ 'number' ]);
      this.ViewportMouseWheel = this.module_.cwrap('ViewportMouseWheel', null, [ 'number', 'number', 'number', 'number', 'number' ]);
      this.ViewportKeyPressed = this.module_.cwrap('ViewportKeyPressed', null, [ 'number', 'number', 'string', 'number', 'number' ]);
      this.ViewportTouchStart = this.module_.cwrap('ViewportTouchStart', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
      this.ViewportTouchMove = this.module_.cwrap('ViewportTouchMove', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
      this.ViewportTouchEnd = this.module_.cwrap('ViewportTouchEnd', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
    }

    public GetCppViewport() : number {
      return this.pimpl_;
    }

    public static GetFromCppViewport(cppViewportHandle: number) : WasmViewport {
      if (WasmViewport.viewportsMapByCppHandle_[cppViewportHandle] !== undefined) {
        return WasmViewport.viewportsMapByCppHandle_[cppViewportHandle];
      }
      Logger.defaultLogger.error("WasmViewport not found !");
      return undefined;
    }

    public static GetFromCanvasId(canvasId: string) : WasmViewport {
      if (WasmViewport.viewportsMapByCanvasId_[canvasId] !== undefined) {
        return WasmViewport.viewportsMapByCanvasId_[canvasId];
      }
      Logger.defaultLogger.error("WasmViewport not found !");
      return undefined;
    }

    public static ResizeAll() {
      for (let canvasId in WasmViewport.viewportsMapByCanvasId_) {
        WasmViewport.viewportsMapByCanvasId_[canvasId].Resize();
      }
    }

    public Redraw() {
      if (this.imageData_ === null ||
          this.renderingBuffer_ === null ||
          this.ViewportRender(this.pimpl_,
                         this.imageData_.width,
                         this.imageData_.height,
                         this.renderingBuffer_) == 0) {
        Logger.defaultLogger.error('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_.HEAPU8.buffer,
          this.renderingBuffer_,
          this.imageData_.width * this.imageData_.height * 4));
        
        this.context_.putImageData(this.imageData_, 0, 0);
      }
    }
  
    public Resize() {
      if (this.imageData_ != null &&
          (this.imageData_.width != window.innerWidth ||
           this.imageData_.height != window.innerHeight)) {
        this.imageData_ = null;
      }
      
      // width/height is defined by the parent width/height
      this.htmlCanvas_.width = this.htmlCanvas_.parentElement.offsetWidth;  
      this.htmlCanvas_.height = this.htmlCanvas_.parentElement.offsetHeight;  

      Logger.defaultLogger.debug("resizing WasmViewport: ", this.htmlCanvas_.width, "x", this.htmlCanvas_.height);

      if (this.imageData_ === null) {
        this.imageData_ = this.context_.getImageData(0, 0, this.htmlCanvas_.width, this.htmlCanvas_.height);
        this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height);
  
        if (this.renderingBuffer_ != null) {
          this.module_._free(this.renderingBuffer_);
        }
        
        this.renderingBuffer_ = this.module_._malloc(this.imageData_.width * this.imageData_.height * 4);
      } else {
        this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height);
      }
      
      this.Redraw();
    }

    public Initialize() {
      
      // Force the rendering of the viewport for the first time
      this.Resize();
    
      var that : WasmViewport = this;
      // Register an event listener to call the Resize() function 
      // each time the window is resized.
      window.addEventListener('resize', function(event) {
        that.Resize();
      }, false);
  
      this.htmlCanvas_.addEventListener('contextmenu', function(event) {
        // Prevent right click on the canvas
        event.preventDefault();
      }, false);
      
      this.htmlCanvas_.addEventListener('mouseleave', function(event) {
        that.ViewportMouseLeave(that.pimpl_);
      });
      
      this.htmlCanvas_.addEventListener('mouseenter', function(event) {
        that.ViewportMouseEnter(that.pimpl_);
      });
    
      this.htmlCanvas_.addEventListener('mousedown', function(event) {
        var x = event.pageX - this.offsetLeft;
        var y = event.pageY - this.offsetTop;

       that.ViewportMouseDown(that.pimpl_, event.button, x, y, 0 /* TODO detect modifier keys*/);    
      });
    
      this.htmlCanvas_.addEventListener('mousemove', function(event) {
        var x = event.pageX - this.offsetLeft;
        var y = event.pageY - this.offsetTop;
        that.ViewportMouseMove(that.pimpl_, x, y);
      });
    
      this.htmlCanvas_.addEventListener('mouseup', function(event) {
        that.ViewportMouseUp(that.pimpl_);
      });
    
      window.addEventListener('keydown', function(event) {
        var keyChar = event.key;
        var keyCode = event.keyCode
        if (keyChar.length == 1) {
          keyCode = 0; // maps to OrthancStone::KeyboardKeys_Generic
        } else {
          keyChar = null;
        }
//        console.log("key: ", keyCode, keyChar);
        that.ViewportKeyPressed(that.pimpl_, keyCode, keyChar, 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(that.pimpl_, event.deltaY, x, y, event.ctrlKey);
        event.preventDefault();
      }, {passive: false}); // must not be passive if calling event.preventDefault, ie to cancel scroll or zoom of the whole interface

      this.htmlCanvas_.addEventListener('touchstart', function(event: TouchEvent) {
        // don't propagate events to the whole body (this could zoom the entire page instead of zooming the viewport)
        event.preventDefault();
        event.stopPropagation();

        // TODO: find a way to pass the coordinates as an array between JS and C++
        var x0 = 0;
        var y0 = 0;
        var x1 = 0;
        var y1 = 0;
        var x2 = 0;
        var y2 = 0;
        if (event.targetTouches.length > 0) {
          x0 = event.targetTouches[0].pageX;
          y0 = event.targetTouches[0].pageY;
        }
        if (event.targetTouches.length > 1) {
          x1 = event.targetTouches[1].pageX;
          y1 = event.targetTouches[1].pageY;
        }
        if (event.targetTouches.length > 2) {
          x2 = event.targetTouches[2].pageX;
          y2 = event.targetTouches[2].pageY;
        }

        that.ViewportTouchStart(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
      }, {passive: false}); // must not be passive if calling event.preventDefault, ie to cancel scroll or zoom of the whole interface
    
      this.htmlCanvas_.addEventListener('touchend', function(event) {
        // don't propagate events to the whole body (this could zoom the entire page instead of zooming the viewport)
        event.preventDefault();
        event.stopPropagation();

        // TODO: find a way to pass the coordinates as an array between JS and C++
        var x0 = 0;
        var y0 = 0;
        var x1 = 0;
        var y1 = 0;
        var x2 = 0;
        var y2 = 0;
        if (event.targetTouches.length > 0) {
          x0 = event.targetTouches[0].pageX;
          y0 = event.targetTouches[0].pageY;
        }
        if (event.targetTouches.length > 1) {
          x1 = event.targetTouches[1].pageX;
          y1 = event.targetTouches[1].pageY;
        }
        if (event.targetTouches.length > 2) {
          x2 = event.targetTouches[2].pageX;
          y2 = event.targetTouches[2].pageY;
        }

        that.ViewportTouchEnd(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
      });
    
      this.htmlCanvas_.addEventListener('touchmove', function(event: TouchEvent) {

        // don't propagate events to the whole body (this could zoom the entire page instead of zooming the viewport)
        event.preventDefault();
        event.stopPropagation();


        // TODO: find a way to pass the coordinates as an array between JS and C++
        var x0 = 0;
        var y0 = 0;
        var x1 = 0;
        var y1 = 0;
        var x2 = 0;
        var y2 = 0;
        if (event.targetTouches.length > 0) {
          x0 = event.targetTouches[0].pageX;
          y0 = event.targetTouches[0].pageY;
        }
        if (event.targetTouches.length > 1) {
          x1 = event.targetTouches[1].pageX;
          y1 = event.targetTouches[1].pageY;
        }
        if (event.targetTouches.length > 2) {
          x2 = event.targetTouches[2].pageX;
          y2 = event.targetTouches[2].pageY;
        }

        that.ViewportTouchMove(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
        return;

      }, {passive: false}); // must not be passive if calling event.preventDefault, ie to cancel scroll or zoom of the whole interface
    }  

  public ResetTouch() {
    if (this.touchTranslation_ ||
        this.touchZoom_) {
      this.ViewportMouseUp(this.pimpl_);
    }

    this.touchTranslation_ = false;
    this.touchZoom_ = false;
  }
  
  public GetTouchTranslation(event) {
    var touch = event.targetTouches[0];
    return [
      touch.pageX,
      touch.pageY
    ];
  }
    
  public GetTouchZoom(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
    ];
  }
   
}