/*
* Copyright (c) 2014 Gwennael Buchet
*
* License/Terms of Use
*
* Permission is hereby granted, free of charge and for the term of intellectual property rights on the Software, to any
* person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify
* and propagate free of charge, anywhere in the world, all or part of the Software subject to the following mandatory conditions:
*
* • The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* Any failure to comply with the above shall automatically terminate the license and be construed as a breach of these
* Terms of Use causing significant harm to Gwennael Buchet.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Gwennael Buchet shall not be used in advertising or otherwise to promote
* the use or other dealings in this Software without prior written authorization from Gwennael Buchet.
*
* These Terms of Use are subject to French law.
* */
"use strict";
/**
* Provides requestAnimationFrame in a cross browser way.
* @property cgsgGlobalRenderingTimer
* @private
* @type {Number}
*/
var cgsgGlobalRenderingTimer = null;
//var cgsgGlobalFramerate = CGSG_DEFAULT_FRAMERATE;
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var x = 0 ; x < vendors.length && !window.requestAnimationFrame ; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 17 - (currTime - lastTime)); //1000/60 = 16.667
cgsgGlobalRenderingTimer = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
//return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
/**
* Represent the scene of the application.
* It encapsulates the scene graph itself and several methods to track mouse and touch events, ...
*
* @class CGSGView
* @constructor
* @module Scene
* @main Scene
* @extends {Object}
* @param {HTMLElement} canvas a handler to the canvas HTML element
* @type {CGSGView}
* @author Gwennael Buchet (gwennael.buchet@gmail.com)
*/
var CGSGView = CGSGObject.extend(
{
initialize : function(canvas) {
//detect the current explorer to apply correct parameters
cgsgDetectCurrentExplorer();
//for IE10 on Win8 : disable the default touch actions
if (typeof canvas.style.msTouchAction != 'undefined') {
canvas.style.msTouchAction = "none";
}
//noinspection JSUndeclaredVariable
CGSG.canvas = canvas;
//noinspection JSUndeclaredVariable
CGSG.context = CGSG.canvas.getContext("2d");
/**
* Multiselection boolean.
* @property allowMultiSelect
* @default true
* @type {Boolean}
*/
this.allowMultiSelect = true;
/**
* Fill color for the drag selection selection rectangle
* @property dragSelectFillColor
* @type {String}
*/
this.dragSelectFillColor = null;
/**
* Stroke color for the drag selection selection rectangle
* @property dragSelectStrokeColor
* @type {String}
*/
this.dragSelectStrokeColor = null;
/**
* Stroke width for the drag selection selection rectangle
* @property dragSelectStrokeWidth
* @type {String}
*/
this.dragSelectStrokeWidth = null;
/**
* Alpha value for the drag selection rectangle
* @property dragSelectAlpha
* @default 0.6
* @type {Number}
*/
this.dragSelectAlpha = CGSG_DEFAULT_DRAG_SELECT_ALPHA;
//noinspection JSUndeclaredVariable
this.dblBuffer = true;
if (this.dblBuffer) {
this._updateDblBuffer();
CGSG.sceneGraph = new CGSGSceneGraph(CGSG.canvas, this._cacheCtx);
}
else {
CGSG.sceneGraph = new CGSGSceneGraph(CGSG.canvas, CGSG.context);
}
/*
* If true, framework will take care of multi-touch : NOT EFFECTIVE YET
* @property multitouch
* @default false
* @type {Boolean}
*/
//this.multitouch = false;
////// @private /////////
/**
* @property _isRunning
* @type {Boolean}
* @private
*/
this._isRunning = false;
// when set to true, the canvas will redraw everything
// invalidateTransformation() just sets this to false right now
// we want to call invalidateTransformation() whenever we make a change
this._needRedraw = true;
/**
* @property _frameContainer Handler to the HTML Element displaying the FPS
* @type {HTMLElement}
* @private
*/
this._frameContainer = null;
/**
* True if the [CTRL} key is being pressed
* @property _keyDownedCtrl
* @default false
* @type {Boolean}
* @private
*/
this._keyDownedCtrl = false;
/**
* @property _timerDblTouch
* @default null
* @type {Number}
* @private
*/
this._timerDblTouch = null;
/**
* The delay between 2 touches to be considered as a dbl touch event.
* To remove the double touch, just set it to 0
* @property dblTouchDelay
* @default CGSG_DEFAULT_DBLTOUCH_DELAY
* @type {Number}
*/
this.dblTouchDelay = CGSG_DEFAULT_DBLTOUCH_DELAY;
/**
* Current positions of the mouse or touch (Array of CGSGPosition)
* @property _mousePos
* @type {Array}
* @private
*/
this._mousePos = [];
this._mouseOldPosition = [];
this._dragStartPos = [];
this._dragEndPos = [];
this._isDrag = false;
this._isResizeDrag = false;
this._isDragSelect = false;
this._resizingDirection = -1;
this._isDblClick = false;
this._mouseUpCount = 0; // counter used to identify dbl click action
this._timeoutDblClick = null;
this._isPressing = false;
//this._frameRatio = 0;
/**
* @property _listCursors List of the names for the cursor when overring a handlebox
* @type {Array}
* @private
*/
this._listCursors =
['nw-resize', 'n-resize', 'ne-resize', 'w-resize', 'e-resize', 'sw-resize',
's-resize', 'se-resize'];
this._offsetX = 0;
this._offsetY = 0;
/**
* @property _selectedNode The current last selected node
* @type {null}
* @private
*/
this._selectedNode = null;
//Experimental : double-buffer for the temporary rendering
/*this._dblCanvas = document.createElement('canvas');
this._dblContext = null;*/
////// INITIALIZATION /////////
//use an external variable to define the scope of the processes
var scope = this;
CGSG.canvas.onmouseout = function(e) {
scope.onMouseOutHandler(e);
};
CGSG.canvas.onmousedown = function(e) {
scope.onMouseDown(e);
};
CGSG.canvas.onmouseup = function(e) {
scope.onMouseUp(e);
};
CGSG.canvas.ondblclick = function(e) {
scope.onMouseDblClick(e);
};
CGSG.canvas.onmousemove = function(e) {
scope.onMouseMove(e);
};
document.onkeydown = function(e) {
scope.onKeyDownHandler(e);
};
document.onkeyup = function(e) {
scope.onKeyUpHandler(e);
};
CGSG.canvas.addEventListener('touchstart', function(e) {
scope.onTouchStart(e);
}, false);
CGSG.canvas.addEventListener('touchmove', function(e) {
scope.onTouchMove(e);
}, false);
CGSG.canvas.addEventListener('touchend', function(e) {
scope.onTouchEnd(e);
}, false);
CGSG.canvas.addEventListener('MSPointerDown', function(e) {
scope.onTouchStart(e);
}, false);
CGSG.canvas.addEventListener("MSPointerMove", function(e) {
scope.onTouchMove(e);
}, false);
CGSG.canvas.addEventListener('MSPointerUp', function(e) {
scope.onTouchEnd(e);
}, false);
this._lastUpdate = new Date().getTime();
this._nodeMouseOver = null;
/**
* Callback on click down on scene event.
* @property onSceneClickStart
* @default null
* @type {Function}
* @example
* this.onSceneClickStart = function (event) {
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onSceneClickStart = null;
/**
* Callback on click up on scene event
* @property onSceneClickEnd
* @default null
* @type {Function}
* @example
* this.onSceneClickEnd = function (event) {
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onSceneClickEnd = null;
/**
* Callback on double click start on scene event
* @property onSceneDblClickStart
* @default null
* @type {Function}
* @example
* this.onSceneDblClickStart = function (event) {
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onSceneDblClickStart = null;
/**
* Callback on double click up on scene event
* @property onSceneDblClickEnd
* @default null
* @type {Function}
* @example
* this.onSceneDblClickEnd = function (event) {
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onSceneDblClickEnd = null;
/**
* Callback on start rendering event
* @property onRenderStart
* @default null
* @type {Function}
* @example
* this.onSceneClickStart = function () {
* //...
* }
*/
this.onRenderStart = null;
/**
* Callback on end rendering event
* @property onRenderEnd
* @default null
* @type {Function}
* @example
* this.onRenderEnd = function () {
* //...
* }
*/
this.onRenderEnd = null;
/**
* Callback on frame average changed event.
* @property onSceneAverageFtpChanged
* @default null
* @type {Function}
* @example
* this.onSceneAverageFtsChanged = function (event) {
* event.fps; // The average FPS }
*/
this.onSceneAverageFpsChanged = null;
//initialize the current frame to 0
//noinspection JSUndeclaredVariable
CGSG.currentFrame = 0;
this._fpss = null;
//if CSS files was declared in <head> tag of index.html file, so we have to ask the framework
// to load all components in cache
this.invalidateTheme();
},
/**
* @method _updateDblBuffer
* @private
*/
_updateDblBuffer : function() {
if (!cgsgExist(this._cacheCanvas)) {
this._cacheCanvas = document.createElement('canvas');
this._cacheCtx = this._cacheCanvas.getContext('2d');
}
this._cacheCanvas.width = CGSG.canvas.width;
this._cacheCanvas.height = CGSG.canvas.height;
cgsgClearContext(this._cacheCtx);
},
/**
* Change the dimension of the canvas.
* Does not really change the dimension of the rendering canvas container,
* but is used by the different computations
* @method setCanvasDimension
* @param d{CGSGDimension} newDimension
* */
setCanvasDimension : function(d) {
CGSG.canvas.width = d.width;
CGSG.canvas.height = d.height;
CGSG.sceneGraph.setCanvasDimension(d);
this._updateDblBuffer();
//Experimental
/*this._dblCanvas.width = newDimension.x;
this._dblCanvas.height = newDimension.y;
this._dblContext = this._dblCanvas.getContext('2d');*/
},
/**
* Remove the nodes selected in the scene graph
* @method deleteSelected
*/
deleteSelected : function() {
if (CGSG.selectedNodes.length > 0) {
//for (var i = CGSG.selectedNodes.length - 1; i >= 0; i--) {
cgsgIterateReverse(CGSG.selectedNodes, (function(i, node) {
this._selectedNode = CGSG.selectedNodes[i];
CGSG.sceneGraph.removeNode(this._selectedNode, true);
}).bind(this));
}
},
/**
* Deselect all nodes
* @public
* @method deselectAll
* @param {Array} excludedArray CGSGNodes not to deselect
*/
deselectAll : function(excludedArray) {
this._isDrag = false;
this._isResizeDrag = false;
this._resizingDirection = -1;
//CGSG.canvas.style.cursor = 'auto';
CGSG.sceneGraph.deselectAll(excludedArray);
this.invalidateTransformation();
},
/**
* the main rendering loop
* @protected
* @method render
*/
render : function() {
if (this._isRunning && this._needRedraw) {
if (this.onRenderStart !== null) {
var evt = new CGSGEvent(this, null);
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_RENDER_START, evt);
//this.onRenderStart();
}
if (this.dblBuffer) {
cgsgClearContext(CGSG.context);
CGSG.context.drawImage(this._cacheCanvas, 0, 0);
}
CGSG.sceneGraph.render();
//render the drag selection box directly onto the scene graph on top of everything else
if (this._dragStartPos.length > 0 && this._dragEndPos.length > 0) {
var p1 = this._dragStartPos[0];
var p2 = this._dragEndPos[0];
var dx = p2.x - p1.x, dy = p2.y - p1.y;
CGSG.sceneGraph.context.save();
CGSG.sceneGraph.context.scale(CGSG.displayRatio.x, CGSG.displayRatio.y);
CGSG.sceneGraph.context.strokeStyle = this.dragSelectStrokeColor;
CGSG.sceneGraph.context.fillStyle = this.dragSelectFillColor;
CGSG.sceneGraph.context.lineWidth = this.dragSelectStrokeWidth;
CGSG.sceneGraph.context.globalAlpha = this.dragSelectAlpha;
CGSG.sceneGraph.context.fillRect(p1.x, p1.y, dx, dy);
CGSG.sceneGraph.context.strokeRect(p1.x, p1.y, dx, dy);
CGSG.sceneGraph.context.restore();
}
if (this.onRenderEnd !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_RENDER_END, new CGSGEvent(this, null));
//this.onRenderEnd();
}
}
//if (!CGSG.sceneGraph.stillHaveAnimation()) {
// this._needRedraw = false;
//}
this._updateFramerate();
this._updateFramerateContainer();
},
/**
* Call this to start the update of the scene
* @public
* @method startPlaying
*/
startPlaying : function() {
//we want the callback of the requestAnimationFrame function to be this one.
//however, the scope of 'this' won't be the same on the requestAnimationFrame function (scope = window)
// and this one (scope = this). So we bind this function to this scope
var bindStartPlaying = this.startPlaying.bind(this);
window.requestAnimationFrame(bindStartPlaying);
this._isRunning = true;
this.render();
},
/**
* Call this to stop the rendering (and so animation) update
* @public
* @method stopPlaying
*/
stopPlaying : function() {
window.cancelAnimationFrame(cgsgGlobalRenderingTimer);
this._isRunning = false;
},
/**
* Inform the SceneGraph that a new render is needed
* @public
* @method invalidateTransformation
*/
invalidateTransformation : function() {
this._needRedraw = true;
},
/**
* Inform the SceneGraph that all nodes must be updated with the current theme
* @method invalidateTheme
*/
invalidateTheme : function() {
CGSG.cssManager.invalidateCache();
this.dragSelectFillColor = CGSG.cssManager.getAttr("cgsg-selectionBox", "background-color");
this.dragSelectStrokeColor = CGSG.cssManager.getAttr("cgsg-selectionBox", "border-color");
this.dragSelectStrokeWidth = CGSG.cssManager.getAttr("cgsg-selectionBox", "border-width");
this.dragSelectAlpha = CGSG.cssManager.getAttr("cgsg-selectionBox", "opacity");
//invalidateTransformation theme for all objects
CGSG.sceneGraph.invalidateTheme();
},
/**
* Update the current framerate
* @method _updateFramerate
* @private
*/
_updateFramerate : function() {
if (!cgsgExist(this._fpss)) {
this._fpss = [];
this.currentFps = 0;
}
var now = new Date().getTime();
var delta = (now - this._lastUpdate);
if (!isNaN(CGSG.maxFramerate)) {
while ((1000.0 / delta) > CGSG.maxFramerate) {
now = new Date().getTime();
delta = (now - this._lastUpdate);
}
}
this._fpss[this.currentFps++] = 1000.0 / delta;
if (this.currentFps == CGSG.framerateDelay) {
this.currentFps = 0;
CGSG.fps = this._fpss.average();
if (this.onSceneAverageFpsChanged !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_SCENE_AVERAGE_FPS_CHANGED,
new CGSGEvent(this, {fps : CGSG.fps}));
}
}
/*if (this._frameRatio === 0) {
this._frameRatio = CGSG.fps;
} else {
this._frameRatio = ((this._frameRatio * (CGSG.currentFrame - 1)) + CGSG.fps) / CGSG.currentFrame;
}*/
this._lastUpdate = now;
},
/**
* Update the innerHTML of the HTMLElement passed as parameter of the "showFPS" function
* @method _updateFramerateContainer
* @private
*/
_updateFramerateContainer : function() {
if (this._frameContainer !== null) {
this._frameContainer.innerHTML = Math.round(CGSG.fps)/*.toString() + " | ~" + (this._frameRatio)*/;
}
},
/**
* @public
* @method showFPS
* @param {HTMLElement} elt an HTML element to receive the FPS. Can be null if you want to remove the framerate
*/
showFPS : function(elt) {
this._frameContainer = elt;
},
/**
* Set the new value for the display ratio.
* The display ratio is used to resize all the elements on the graph to be adapted to the screen,
* depending on the reference screen size.
* You can compute the ratio like this: x = canvas.width/reference.width ; y = canvas.height/reference.height
* @public
* @method setDisplayRatio
* @param ratio {CGSGScale} a CGSGScale value
*/
setDisplayRatio : function(ratio) {
//noinspection JSUndeclaredVariable
CGSG.displayRatio = ratio;
CGSG.sceneGraph.initializeGhost(CGSG.canvas.width / CGSG.displayRatio.x,
CGSG.canvas.height / CGSG.displayRatio.y);
},
/**
* Detects when the mouse leaves the canvas.
* @method onMouseOutHandler
* @param e {MouseEvent} the event
*/
onMouseOutHandler : function(e) {
this._isPressing = false;
},
/**
* click mouse Event handler function
* @protected
* @method onMouseDown
* @param e {MouseEvent} event
*/
onMouseDown : function(e) {
this.onTouchStart(e);
},
/**
* touch down Event handler function
* @protected
* @method onTouchStart
* @param e {Event} event
*/
onTouchStart : function(e) {
this._isPressing = true;
if (cgsgExist(this._mousePos)) {
this._mouseOldPosition = this._mousePos.copy();
}
this._mousePos = cgsgGetCursorPositions(e, CGSG.canvas);
this._selectedNode = CGSG.sceneGraph.pickNode(this._mousePos[0], null);
if (cgsgExist(this._selectedNode)) {
if (this._selectedNode.onClickStart) {
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_CLICK_START,
new CGSGEvent(this,
{nativeEvent : e, position : this._mousePos}));
}
this._mouseOldPosition = this._mousePos.copy();
}
this._updateSelection(e);
},
/**
* Updates the current selection according to the given event.
*
* @method _updateSelection
* @param e {Event} the event
* @private
*/
_updateSelection : function(e) {
//if a node is under the cursor, select it if it is (clickable || resizable || draggable)
//s = selectable
var s = cgsgExist(this._selectedNode) &&
(this._selectedNode.isClickable || this._selectedNode.isDraggable ||
this._selectedNode.isResizable);
if (s) {
if (this._selectedNode.isDraggable || this._selectedNode.isResizable) {
//if multiselection is activated
if (this.allowMultiSelect && this._keyDownedCtrl) {
if (!this._selectedNode.isSelected) {
CGSG.sceneGraph.selectNode(this._selectedNode);
}
else {
CGSG.sceneGraph.deselectNode(this._selectedNode);
}
}
//no multiselection
else {
//if node not already selected
if (!this._selectedNode.isSelected) {
this.deselectAll(null);
CGSG.sceneGraph.selectNode(this._selectedNode);
}
}
this._isDrag = !this._detectResizeMode(this._mousePos[0]);
//ask for redraw
this.invalidateTransformation();
}
}
//else if no nodes was clicked
else {
this.deselectAll(null);
}
// Check if we can start drag selection
var canStartDragSelection = this._canStartDragSelection(e);
// if the _canStartDragSelection has not been sub classed : apply this rule :
// if no nodes were hit (that were clickable,resizeable or draggable) lets start a drag selection if we are allowed
if (!cgsgExist(canStartDragSelection)) {
canStartDragSelection = !s;
}
if (this.allowMultiSelect && canStartDragSelection) {
var p = cgsgGetCursorPositions(e, CGSG.canvas);
this._isDragSelect = true;
this._dragStartPos = p;
this._dragEndPos = p;
this.deselectAll(null);
}
},
/**
* This method indicates if, according to the current state of the scene, a drag selection could starts. Called
* when a touchStart event triggered. Could be overridden to specify different behaviour.
*
* @method _canStartDragSelection
* @protected
* @param e {Event} the event
* @return {Boolean} true if drag selection could starts, false otherwise
*/
_canStartDragSelection : function(e) {
// tells the caller to use default behaviour by returning nothing (undefined)
},
/**
* Dispatch a 'click' event and for any selected node which is clickable and and only if 'this._isDblClick' == false.
*
* @method _dispatchClick
* @param event {CGSGEvent} the event to dispatch
* @private
*/
_dispatchClick : function(e) {
//execute the action bound with the click e
if (cgsgExist(this._selectedNode) && this._selectedNode.isClickable) {
if (!this._isDblClick && cgsgExist(this._selectedNode.onClick)) {
e.data.node = this._selectedNode;
e.data.positions = this._mousePos.copy();
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_CLICK, e);
//this._selectedNode.onClick({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
//deselect all node except the new _selectedNode
if (this._selectedNode.isDraggable === false && this._selectedNode.isResizable === false) {
this.deselectAll([this._selectedNode]);
}
}
},
/**
* Click on the scene
*
* @private
* @method _clickOnScene
* @param e {CGSGEvent} wrapper of MouseEvent or TouchEvent
* @param {Boolean} pickNode
*/
_clickOnScene : function(e, pickNode) {
this._mousePos = cgsgGetCursorPositions(e.data.nativeEvent, CGSG.canvas);
if (this.onSceneClickStart !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_SCENE_CLICK_START, e);
//this.onSceneClickStart({positions: this._mousePos.copy(), e: e});
}
//try to pick up the nodes under the cursor
if (pickNode) {
this._selectedNode = CGSG.sceneGraph.pickNode(this._mousePos[0], function(node) {
return (node.isTraversable === true && (node.isClickable === true || node.isDraggable === true
|| node.isResizable === true));
});
}
//this._updateSelection(e);
this._dispatchClick(e);
if (this.onSceneClickEnd !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_SCENE_CLICK_END, e);
//this.onSceneClickEnd({positions: this._mousePos.copy(), e: e});
}
//this._mouseOldPosition = this._mousePos.copy();
},
/**
* mouse move Event handler function
* @protected
* @method onMouseMove
* @param e {MouseEvent} event
*/
onMouseMove : function(e) {
this._moveOnScene(e);
},
/**
* touch move Event handler function
* @protected
* @method onTouchMove
* @param {Event} event
*/
onTouchMove : function(e) {
if (e.preventManipulation)
e.preventManipulation();
e.preventDefault();
e.stopPropagation();
this._moveOnScene(e);
},
/**
* @private
* @method _moveOnScene
* @param e {Event} MouseEvent or TouchEvent
*/
_moveOnScene : function(e) {
var i, offX, offY;
this._mousePos = cgsgGetCursorPositions(e, CGSG.canvas);
var selN = this._selectedNode;
this._selectedNode = null;
if (this._isPressing && this._isDrag) {
if (CGSG.selectedNodes.length > 0) {
var mp = this._mousePos[0];
var mop = this._mouseOldPosition[0];
this._offsetX = mp.x - mop.x;
this._offsetY = mp.y - mop.y;
var canMove = true;
for (i = CGSG.selectedNodes.length - 1 ; i >= 0 ; i--) {
this._selectedNode = CGSG.selectedNodes[i];
if (this._selectedNode !== null && this._selectedNode.isDraggable) {
this._selectedNode.isMoving = true;
//TODO : appliquer aussi l'opposée de la rotation
offX = this._offsetX /
(this._selectedNode._absSca.x / this._selectedNode.scale.x);
offY = this._offsetY /
(this._selectedNode._absSca.y / this._selectedNode.scale.y);
//check for the region constraint
if (this._selectedNode.regionConstraint !== null) {
var reg = this._selectedNode.getRegion().copy();
reg.position.x += offX;
reg.position.y += offY;
if (!cgsgRegionIsInRegion(reg, this._selectedNode.regionConstraint, 0)) {
canMove = false;
}
}
if (canMove) {
this._selectedNode.translateWith(offX, offY);
if (this._selectedNode.onDrag !== null) {
var evt = new CGSGEvent(this,
{node : this._selectedNode, positions : this._mousePos.copy(), nativeEvent : e});
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_DRAG, evt);
//this._selectedNode.onDrag({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
}
}
}
this._mouseOldPosition = this._mousePos.copy();
// something is changing position so we better invalidateTransformation the canvas!
this.invalidateTransformation();
}
}
else if (this._isPressing && this._isResizeDrag) {
if (CGSG.selectedNodes.length > 0) {
var mp = this._mousePos[0];
var mop = this._mouseOldPosition[0];
this._offsetX = mp.x - mop.x;
this._offsetY = mp.y - mop.y;
//for (i = CGSG.selectedNodes.length - 1; i >= 0; i--) {
cgsgIterateReverse(CGSG.selectedNodes, (function(i, node) {
this._selectedNode = node;//CGSG.selectedNodes[i];
if (this._selectedNode.isResizable) {
this._selectedNode.isResizing = true;
//TODO : appliquer aussi l'opposée de la rotation
offX = this._offsetX / this._selectedNode._absSca.x;
offY = this._offsetY / this._selectedNode._absSca.y;
var delta = Math.max(offX, offY);
if (delta == 0) {
delta = Math.min(offX, offY);
}
var realDimX = this._selectedNode.dimension.width *
this._selectedNode._absSca.x;
var realDimY = this._selectedNode.dimension.height *
this._selectedNode._absSca.y;
var d = {dW : 0, dH : 0};
// 0 1 2
// 3 4
// 5 6 7
switch (this._resizingDirection) {
case 0:
if (this._selectedNode.isProportionalResize) {
d = this._getDeltaOnMove(delta, offX, offY, realDimX,
realDimY,
-1, -1);
this._selectedNode.translateWith(-d.dW, -d.dH, false);
this._selectedNode.resizeWith(d.dW, d.dH, false);
}
else {
this._selectedNode.translateWith(offX * this._selectedNode.scale.x,
offY * this._selectedNode.scale.y,
false);
this._selectedNode.resizeWith(-offX, -offY, false);
}
break;
case 1:
this._selectedNode.translateWith(0, offY * this._selectedNode.scale.y,
false);
this._selectedNode.resizeWith(0, -offY, false);
break;
case 2:
if (this._selectedNode.isProportionalResize) {
d = this._getDeltaOnMove(delta, offX, offY, realDimX,
realDimY,
1, -1);
this._selectedNode.translateWith(0, -d.dH, false);
this._selectedNode.resizeWith(d.dW, d.dH, false);
}
else {
this._selectedNode.translateWith(0, offY * this._selectedNode.scale.y,
false);
this._selectedNode.resizeWith(offX, -offY, false);
}
break;
case 3:
this._selectedNode.translateWith(offX * this._selectedNode.scale.x, 0,
false);
this._selectedNode.resizeWith(-offX, 0, false);
break;
case 4:
this._selectedNode.resizeWith(offX, 0, false);
break;
case 5:
if (this._selectedNode.isProportionalResize) {
d = this._getDeltaOnMove(delta, offX, offY, realDimX,
realDimY,
1, -1);
this._selectedNode.translateWith(d.dW, 0, false);
this._selectedNode.resizeWith(-d.dW, -d.dH, false);
}
else {
this._selectedNode.translateWith(offX * this._selectedNode.scale.x, 0,
false);
this._selectedNode.resizeWith(-offX, offY, false);
}
break;
case 6:
this._selectedNode.resizeWith(0, offY, false);
break;
case 7:
if (this._selectedNode.isProportionalResize) {
d = this._getDeltaOnMove(delta, offX, offY, realDimX,
realDimY,
1, 1);
this._selectedNode.resizeWith(d.dW, d.dH, false);
}
else {
this._selectedNode.resizeWith(offX, offY, false);
}
break;
}
this._selectedNode.computeAbsoluteMatrix(false);
if (this._selectedNode.onResize !== null) {
var evt = new CGSGEvent(this,
{node : this._selectedNode, positions : this._mousePos.copy(), e : e});
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_RESIZE, evt);
//this._selectedNode.onResize({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
}
}).bind(this));
}
this._mouseOldPosition = this._mousePos.copy();
this.invalidateTransformation();
}
// if there's a selection, see if we grabbed one of the resize handles
else if (CGSG.selectedNodes.length > 0/* && this._isResizeDrag == false*/) {
if (this._detectResizeMode(this._mousePos[0])) {
return;
}
else {
// not over a selection box, return to normal
//this._isResizeDrag = false;
this._resizingDirection = -1;
CGSG.canvas.style.cursor = 'auto';
//ask for redraw
this.invalidateTransformation();
}
}
//if we are drag selecting
else if (this._isDragSelect) {
this._dragEndPos = this._mousePos.copy();
//ask to redraw for the selection box
this.invalidateTransformation();
}
//mouse over a node ?
if (!this._isDrag && !this._isResizeDrag) {
var n = null;
//first test the mouse over the current _nodeMouseOver. If it's ok, no need to traverse other
if (cgsgExist(this._nodeMouseOver)) {
n = this._nodeMouseOver.pickNode(this._mousePos[0], null, CGSG.ghostContext, false, null);
if (n === null) {
this._nodeMouseOver.isMouseOver = false;
if (cgsgExist(this._nodeMouseOver.onMouseOut)) {
var evt = new CGSGEvent(this,
{node : this._nodeMouseOver, positions : this._mousePos.copy(), e : e});
CGSG.eventManager.dispatch(this._nodeMouseOver, cgsgEventTypes.ON_MOUSE_OUT, evt);
//this._nodeMouseOver.onMouseOut({node: this._nodeMouseOver, positions: this._mousePos.copy(), e: e});
}
this._nodeMouseOver = null;
}
else if (n === this._nodeMouseOver) {
if (cgsgExist(this._nodeMouseOver.onMouseOver)) {
var evt = new CGSGEvent(this,
{node : this._nodeMouseOver, positions : this._mousePos.copy(), e : e});
CGSG.eventManager.dispatch(this._nodeMouseOver, cgsgEventTypes.ON_MOUSE_OVER, evt);
//this._nodeMouseOver.onMouseOver({node: this._nodeMouseOver, positions: this._mousePos.copy(), e: e});
}
}
}
//if the previous node under the mouse is no more under the mouse, test the other nodes
if (n === null) {
if ((n = CGSG.sceneGraph.pickNode(this._mousePos[0], function(node) {
return (node.onMouseEnter !== null || node.onMouseOver !== null)
})) !== null) {
n.isMouseOver = true;
this._nodeMouseOver = n;
this._nodeMouseOver.isMouseOver = true;
if (cgsgExist(this._nodeMouseOver.onMouseEnter)) {
var evt = new CGSGEvent(this,
{node : this._nodeMouseOver, positions : this._mousePos.copy(), e : e});
CGSG.eventManager.dispatch(this._nodeMouseOver, cgsgEventTypes.ON_MOUSE_ENTER, evt);
//this._nodeMouseOver.onMouseEnter({node: this._nodeMouseOver, positions: this._mousePos.copy(), e: e})
}
}
}
}
this._selectedNode = selN;
},
/**
* Detects if the mouse if over the handle box of a selected node.
*
* @method _detectResizeMode
* @param mousePosition {CGSGPosition} the cursor position
* @return {Boolean} true if we resize, false otherwise
* @private
*/
_detectResizeMode : function(pos) {
var sel = this._selectedNode;
cgsgIterateReverse(CGSG.selectedNodes, (function(i, node) {
this._selectedNode = node;
if (this._selectedNode.isResizable) {
for (var h = 0 ; h < 8 ; h++) {
if (node.isProportionalResizeOnly && (h == 1 || h == 3 || h == 4 || h == 6))
continue;
var selectionHandle = this._selectedNode.handles[h];
this._isResizeDrag = selectionHandle.checkIfSelected(pos, CGSG.resizeHandleThreshold);
// resize handles will always be rectangles
if (this._isResizeDrag) {
// we found one!
this._resizingDirection = h;
//draw the correct cursor
CGSG.canvas.style.cursor = this._listCursors[h];
//if the mouse cursor is over a handle box (ie: a resize marker)
// if (this._resizingDirection !== -1) {
// this._isResizeDrag = true;
//}
return false;
}
}
}
}).bind(this));
this._selectedNode = sel;
return this._isResizeDrag;
},
/**
* @method _getDeltaOnMove
* @param delta {Number}
* @param offX {Number} nodeOffsetX
* @param offY {Number} nodeOffsetY
* @param w {Number}
* @param h {Number}
* @param signeX {Number}
* @param signeY {Number}
* @return {Object}
* @private
*/
_getDeltaOnMove : function(delta, offX, offY, w, h, signeX, signeY) {
var dW = offX, dH = offY;
var r = 1.0;
if (delta == offX) {
r = (w + signeX * delta) / w;
dW = signeX * delta;
dH = (r - 1.0) * h;
}
else {
r = (h + signeY * delta) / h;
dH = signeY * delta;
dW = (r - 1.0) * w;
}
return {dW : dW, dH : dH};
},
/**
* mouse up Event handler function
* @protected
* @method onMouseUp
* @param {MouseEvent} event
*/
onMouseUp : function(e) {
this.onTouchEnd(e);
},
/**
* touch up Event handler function
* @protected
* @method onTouchEnd
* @param {Event} event
*/
onTouchEnd : function(e) {
if (e.preventManipulation)
e.preventManipulation();
e.preventDefault();
e.stopPropagation();
this._isPressing = false;
this._mouseUpCount++;
//if the touch was over a node with the onDblClick method defined, check whether it's a dbl touch or not
if (cgsgExist(this._selectedNode) && cgsgExist(this._selectedNode.onDblClick)) {
//if the timer exists, then it's a dbl touch
if (this._mouseUpCount > 1) {
clearTimeout(this._timeoutDblClick);
this._upAndDblClick(e);
}
else {
this._timeoutDblClick = setTimeout((function() {
this._upAndClick(e);
}).bind(this), this.dblTouchDelay);
}
}
else {
// not a touch on a node with the onDblClick e defined,
// so it's a single touch, just call the _clickOnScene method usually
this._upAndClick(e);
}
},
/**
* Creates the custom event by calling _upOnScene and then call _clickOnScene.
*
* @method _upAndClick
* @param {Event} event the event
* @private
*/
_upAndClick : function(e) {
this._mouseUpCount = 0;
var ed = this._upOnScene(e);
ed.nativeEvent = e;
this._clickOnScene(new CGSGEvent(this, ed), false);
},
/**
* Creates the custom event by calling _upOnScene and then call _dblClickOnScene.
*
* @method _upAndDblClick
* @param {Event} event the event
* @private
*/
_upAndDblClick : function(e) {
this._mouseUpCount = 0;
var eventData = this._upOnScene(e);
eventData.nativeEvent = e;
this._dblClickOnScene(new CGSGEvent(this, eventData), false);
},
/**
* @method _upOnScene
* @param {Event} event MouseEvent or TouchEvent
* @return {Object} a structure indicating is the node has been moved or resize
* @private
*/
_upOnScene : function(e) {
var sel = this._selectedNode;
var i = 0;
var retval = {};
retval.nativeEvent = e;
var exist = cgsgExist(sel);
retval.hasMoved = exist && sel.isMoving;
retval.hasResize = exist && sel.isResizing;
//if current action was to drag nodes
if (this._isDrag) {
cgsgIterateReverse(CGSG.selectedNodes, (function(i, node) {
//for (i = CGSG.selectedNodes.length - 1; i >= 0; i--) {
// this._selectedNode = CGSG.selectedNodes[i];
this._selectedNode = node;
if (this._selectedNode.isMoving) {
this._selectedNode.isMoving = false;
this._selectedNode.computeAbsoluteMatrix(true);
if (this._selectedNode.onDragEnd !== null) {
var evt = new CGSGEvent(this,
{node : this._selectedNode, positions : this._mousePos.copy(), nativeEvent : e});
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_DRAG_END, evt);
// this._selectedNode.onDragEnd({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
}
//}
}).bind(this));
this._isDrag = false;
}
//else if current action was to resize nodes
else if (this._isResizeDrag) {
cgsgIterateReverse(CGSG.selectedNodes, (function(i, node) {
//for (i = CGSG.selectedNodes.length - 1; i >= 0; i--) {
//this._selectedNode = CGSG.selectedNodes[i];
this._selectedNode = node;
if (this._selectedNode.isResizing) {
this._selectedNode.isResizing = false;
this._selectedNode.computeAbsoluteMatrix(true);
if (this._selectedNode.onResizeEnd !== null) {
var evt = new CGSGEvent(this,
{node : this._selectedNode, positions : this._mousePos.copy(), nativeEvent : e});
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_RESIZE_END, evt);
//this._selectedNode.onResizeEnd({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
}
//}
}).bind(this));
this._isResizeDrag = false;
}
//else if this is a drag select
else if (this._isDragSelect) {
this._isDragSelect = false;
this._doDragSelect();
this._dragStartPos = [];
this._dragEndPos = [];
//request a re-render for the drag select rect to be killed with
this.invalidateTransformation();
}
//else if just up the mice of nodes
else {
this._selectedNode = CGSG.selectedNodes[CGSG.selectedNodes.length - 1];
if (cgsgExist(this._selectedNode) && this._selectedNode.onMouseUp !== null) {
var evt = new CGSGEvent(this,
{node : this._selectedNode, positions : this._mousePos.copy(), e : e});
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_MOUSE_UP, evt);
//this._selectedNode.onMouseUp({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
}
this._resizingDirection = -1;
this._selectedNode = sel;
return retval;
},
/**
* Select the nodes under the drag select rectangle
* @protected
* @method _doDragSelect
*/
_doDragSelect : function() {
var p1 = this._dragStartPos[0];
var p2 = this._dragEndPos[0];
var dx = p2.x - p1.x, dy = p2.y - p1.y;
if (dx < 0) {
dx = Math.abs(dx);
p1.x -= dx;
}
if (dy < 0) {
dy = Math.abs(dy);
p1.y -= dy;
}
var region = new CGSGRegion(p1.x, p1.y, dx, dy);
var newSelections = CGSG.sceneGraph.pickNodes(region, function(node) {
return (node.isTraversable === true && (/*node.isClickable === true ||*/ node.isDraggable === true
|| node.isResizable === true))
});
for (var i = 0, len = newSelections.length ; i < len ; ++i) {
CGSG.sceneGraph.selectNode(newSelections[i]);
}
},
/**
* mouse double click Event handler function
* @protected
* @method onMouseDblClick
* @param {MouseEvent} event
*/
onMouseDblClick : function(event) {
//this._dblClickOnScene(event);
event.preventDefault();
event.stopPropagation();
},
/**
* @protected
* @method _dblClickOnScene
* @param {CGSGEvent} event wrapping the native event
* @param {Boolean} mustPickNode
* @return {CGSGNode} the node that was double-clicked
* @private
*/
_dblClickOnScene : function(e, mustPickNode) {
//this._updateSelection(e);
if (this.onSceneDblClickStart !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_SCENE_DBL_CLICK_START, e);
//this.onSceneDblClickStart(e);
}
if (mustPickNode) {
//this._mousePos = cgsgGetCursorPositions(e, CGSG.canvas);
this._selectedNode = CGSG.sceneGraph.pickNode(this._mousePos[0], function(node) {
return true;
});
}
if (cgsgExist(this._selectedNode) && this._selectedNode.onDblClick !== null) {
e.data.node = this._selectedNode;
e.data.positions = this._mousePos.copy();
CGSG.eventManager.dispatch(this._selectedNode, cgsgEventTypes.ON_DBL_CLICK, e);
//this._selectedNode.onDblClick({node: this._selectedNode, positions: this._mousePos.copy(), e: e});
}
else if (this.onSceneDblClickEnd !== null) {
CGSG.eventManager.dispatch(this, cgsgEventTypes.ON_SCENE_DBL_CLICK_END, e);
//this.onSceneDblClickEnd({positions: this._mousePos.copy(), e: e});
}
return this._selectedNode;
},
/**
* @method onKeyDownHandler
* @protected
* @param {KeyboardEvent} event
* @return {Number}
*/
onKeyDownHandler : function(e) {
var keynum = (window.e) ? e.keyCode : e.which;
switch (keynum) {
case 17:
this._keyDownedCtrl = true;
break;
}
return keynum;
},
/**
* @method onKeyUpHandler
* @protected
* @param {KeyboardEvent} event
* @return {Number}
*/
onKeyUpHandler : function(e) {
var keynum = (window.e) ? e.keyCode : e.which;
switch (keynum) {
case 17:
this._keyDownedCtrl = false;
break;
}
return keynum;
}
}
);