/*
* Copyright (c) 2012 Capgemini Technology Services (hereinafter “Capgemini”)
*
* 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 Capgemini.
*
* 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 Capgemini shall not be used in advertising or otherwise to promote
* the use or other dealings in this Software without prior written authorization from Capgemini.
*
* These Terms of Use are subject to French law.
*/
"use strict";
/**
* Base class for a Node in the Scene Graph.
* Each node encapsulates its position, dimension, scale and rotation, ...
* @class CGSGNode
* @extends Object
* @module Node
* @main Node
* @constructor
* @param {Number} x Relative position on X
* @param {Number} y Relative position on Y
* @param {Number} width Relative dimension
* @param {Number} height Relative Dimension
* @type {CGSGNode}
* @author Gwennael Buchet (gwennael.buchet@gmail.com)
*/
var CGSGNode = CGSGObject.extend(
{
initialize: function (x, y, width, height) {
/**
* The name of this nodes. Should be unique, but no control is done.
* @property name
* @default ""
* @type {String}
*/
this.name = "";
/**
* Indicate whether this node is selected or not.
* Use CGSGScene::scenegraph.selectNode(nodeToSelect) to select a node
* @property isSelected
* @readonly
* @default false
* @type {Boolean}
*/
this.isSelected = false;
/**
* The type of this class. Must be redefined by inherited classes
* @property classType
* @readonly
* @type {String}
*/
this.classType = "CGSGNODE";
/**
* The 8 handleboxes that will be the resize handles
* the resize handles will be in this order:
* 0 1 2
* 3 4
* 5 6 7
* @property resizeHandles
* @readonly
* @type {Array}
*/
this.resizeHandles = [];
/**
* Level of transparency of the node.
* @default 1.0
* @property globalAlpha
* @type {Number}
*/
this.globalAlpha = 1.0;
/**
* Indicate if the node is visible (and so selectable) or not
* @property isVisible
* @default true
* @type {Boolean}
*/
this.isVisible = true;
/**
* If true, the node will be proportionally resized
* @property isProportionalResize
* @type {Boolean}
*/
this.isProportionalResize = false;
/**
* Define the method the detection (or "pick") method will be used for this node.
* Possible values CGSGPickNodeMethod.REGION and CGSGPickNodeMethod.GHOST.
*
* <ul>
* <li>REGION : the detection returns true if the mouse cursor is inside the bounding box of the node</li>
* <li>GHOST : the detection will use the "renderGhost" method of the node to achieve a more accurate detection</li>
* </ul>
*
* @property pickNodeMethod
* @default CGSGPickNodeMethod.REGION
* @type {CGSGPickNodeMethod}
*/
this.pickNodeMethod = CGSGPickNodeMethod.REGION;
/**
* List of the children (empty if this nodes is a leaf)
* @property children
* @readonly
* @type {Array}
*/
this.children = [];
/**
* The constraint region when moving the node
* @property regionConstraint
* @default null
* @type {null}
*/
this.regionConstraint = null;
/**
* Pivot point to apply a rotation.
* The point is a value between [0, 0] and [1, 1].
* [0, 0] is the top left corner of the bounding box and [1, 1] the bottom right corner.
* @property rotationCenter
* @default null
* @type {CGSGPosition}
*/
this.rotationCenter = null;
/**
* can be fulfilled by the developer to put in whatever he needs
* @property userdata
* @default null
* @type {*}
*/
this.userdata = null;
/**
* selection attributes
* If true, this node is clickable and so will be checked by the pickNode function
* @property isClickable
* @default true
* @type {Boolean}
*/
this.isClickable = true;
/**
* If true, this node can be resized by the user. In that case, the dimension property will be affected, not the scale one.
* @property isResizable
* @default false
* @type {Boolean}
*/
this.isResizable = false;
/**
* If true, the node can be dragged by the user
* @property isDraggable
* @default false
* @type {Boolean}
*/
this.isDraggable = false;
/**
* If true, the absolute matrix will be recomputed after each movement (and so in animation).
* Set it to false to gain performance if you don't need to keep trace of absolute position (no need to collision, picknode, ...)
* @property needToKeepAbsoluteMatrix
* @default true
* @type {Boolean}
*/
this.needToKeepAbsoluteMatrix = true;
/**
* Color for the line around this node when selected
* @property selectionLineColor
* @default "#FF6890"
* @type {String}
*/
this.selectionLineColor = CGSG_DEFAULT_SELECTED_STROKE_COLOR;
/**
* Width for the line around this node when selected
* @property selectionLineWidth
* @default 2
* @type {Number}
*/
this.selectionLineWidth = CGSG_DEFAULT_SELECTED_STROKE_SIZE;
/**
* Color for the handle boxes around this node when selected
* @property selectionHandleSize
* @default 6
* @type {Number}
*/
this.selectionHandleSize = CGSG_DEFAULT_SELECTED_RESIZEHANDLE_SIZE;
/**
* Color for the handle boxes around this node when selected
* @property selectionHandleColor
* @default "#9068FF""
* @type {String}
*/
this.selectionHandleColor = CGSG_DEFAULT_SELECTED_RESIZEHANDLE_COLOR;
/**
* Updated by the scene itself. Don't update it manually.
* True if the mice is over the node, false otherwise
* @property isMouseOver
* @readonly
* @type {Boolean}
*/
this.isMouseOver = false;
/**
* Updated by the scene itself. Don't update it manually.
* True if the node is being moved manually, false otherwise
* @property isMoving
* @readonly
* @type {Boolean}
*/
this.isMoving = false;
/**
* Updated by the scene itself. Don't update it manually.
* True if the node is being resized manually, false otherwise
* @property isResizing
* @readonly
* @type {Boolean}
*/
this.isResizing = false;
/**
* ID for the node. Should be filled by the developer. The framework will never use it.
* @property _id
* @type {Number}
* @private
*/
this._id = 0;
/**
* parent of this node
* @property _parentNode
* @type {CGSGNode}
* @private
*/
this._parentNode = null;
/**
* Relative position of this nodes on the canvas container, relatively to the position of its parent node.
* Never use it to move the node, use translateBy/translateWith/translateTo instead
* @readonly
* @property position
* @default CGSGPosition(0, 0)
* @type {CGSGPosition}
*/
this.position = new CGSGPosition(0, 0);
/**
* Absolute position of this nodes on the canvas container. Generated value. Don't modify it manually
* Never use it to move the node, use translateBy/translateWith/translateTo instead
* @readonly
* @property _absolutePosition
* @private
* @type {CGSGPosition}
*/
this._absolutePosition = new CGSGPosition(0, 0);
/**
* Dimension of this nodes on the canvas container
* Never use it to resize the node, use resizeBy/resizeWith/resizeTo instead
* @readonly
* @property dimension
* @default CGSGDimension(0, 0)
* @type {CGSGDimension}
*/
this.dimension = new CGSGDimension(0, 0);
/**
* Relative scale of this nodes on the canvas container, relatively to the scale of its parent node.
* Never use it to scale or resize the node, use scaleBy/scaleWith/scaleTo instead
* @readonly
* @property scale
* @default CGSGScale(1, 1)
* @type {CGSGScale}
*/
this.scale = new CGSGScale(1, 1);
/**
* Absolute scale of this nodes on the canvas container. Generated value. Don't modify it manually
* Never use it to scale the node, use scaleBy/scaleWith/scaleTo instead
* @readonly
* @property _absoluteScale
* @private
* @type {CGSGScale}
*/
this._absoluteScale = new CGSGScale(1, 1);
/**
* Relative rotation of this nodes on the canvas container, relatively to the rotation of its parent node.
* Never use it to rotate or resize the node, use rotateBy/rotateWith/rotateTo instead
* @readonly
* @property rotation
* @default CGSGRotation(0)
* @type {CGSGRotation}
*/
this.rotation = new CGSGRotation(0);
/**
* Absolute rotation of this nodes on the canvas container. Generated value. Don't modify it manually
* Never use it to rotate or resize the node, use rotateBy/rotateWith/rotateTo instead
* @readonly
* @private
* @property _absoluteRotation
* @type {CGSGRotation}
*/
this._absoluteRotation = new CGSGRotation(0);
/**
* @property _isDrag
* @type {Boolean}
* @private
*/
this._isDrag = false;
//this.selectableZone =
//new CGSGRegion(this.position.x, this.position.y, this.dimension.width, this.dimension.height);
/**
* true if this node is traversable (recursively) (ie : by the picknode, a traverser, ...)
* @property isTraversable
* @type {Boolean}
*/
this.isTraversable = true;
/**
* Indicate if this node is managed by the collision manager
* @property isCollisionManaged
* @type {Boolean}
*/
this.isCollisionManaged = false;
//initialize the position and dimension
this.translateTo(x, y, true);
this.resizeTo(width, height);
// initialize the selection handleBoxes
for (var i = 0; i < 8; i++) {
var handleBox = new CGSGHandleBox(this, this.selectionHandleSize, this.selectionHandleColor,
this.selectionLineColor, this.selectionLineWidth, 0, 0);
this.resizeHandles.push(handleBox);
}
/**
* Callback on mouse over the node
* @property onMouseOver
* @default null
* @type {function}
*
* @example
* this.onMouseOver = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onMouseOver = null;
/**
* Callback on mouse enter on the node
* @property onMouseEnter
* @default null
* @type {function}
*
* @example
* this.onMouseEnter = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onMouseEnter = null;
/**
* Callback on mouse out
* @property onMouseOut
* @default null
* @type {function}
*
* @example
* this.onMouseOut = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onMouseOut = null;
/**
* Callback on mouse up
* @property onMouseUp
* @default null
* @type {function}
*
* @example
* this.onMouseUp = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onMouseUp = null;
/**
* Callback on mouse or touch click
* @property onClick
* @default null
* @type {function}
*
* @example
* this.onClick = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onClick = null;
/**
* Callback on mouse or touch double click
* @property onDblClick
* @default null
* @type {function}
*
* @example
* this.onDblClick = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onDblClick = null;
/**
* Callback on drag this node
* @property onDrag
* @default null
* @type {function}
*
* @example
* this.onDrag = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onDrag = null;
/**
* Callback on end of drag this node
* @property onDragEnd
* @default null
* @type {function}
*
* @example
* this.onDragEnd = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onDragEnd = null;
/**
* Callback on resize this node
* @property onResize
* @default null
* @type {function}
*
* @example
* this.onResize = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onResize = null;
/**
* Callback on end resize this node
* @property onResizeEnd
* @default null
* @type {function}
*
* @example
* this.onResizeEnd = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onResizeEnd = null;
/**
* Callback on select this node
* @property onSelect
* @default null
* @type {function}
*
* @example
* this.onSelect = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onSelect = null;
/**
* Callback on deselect this node
* @property onDeselect
* @default null
* @type {function}
*
* @example
* this.onDeselect = function (event) {
* event.node; //CGSGNode
* event.position; //Array of CGSGPosition
* event.event; //Event
* }
*/
this.onDeselect = null;
this.computeAbsoluteMatrix(true);
},
/**
* return the relative region of this node
* @public
* @method getRegion
* @return {CGSGRegion}
*/
getRegion: function () {
return new CGSGRegion(this.position.x, this.position.y, this.getWidth(), this.getHeight());
},
/**
* return the absolute region of this node
* @public
* @method getAbsoluteRegion
* @return {CGSGRegion}
*/
getAbsoluteRegion: function () {
return new CGSGRegion(this.getAbsoluteLeft(), this.getAbsoluteTop(), this.getAbsoluteWidth(),
this.getAbsoluteHeight());
},
//// RENDERING MANIPULATION //////
/**
* Wipes the canvas context
* @method _clearContext
* @param context
* @param canvasWidth
* @param canvasHeight
* @private
*/
_clearContext: function (context, canvasWidth, canvasHeight) {
context.clearRect(0, 0, canvasWidth, canvasHeight);
},
/**
* Empty rendering function. Must be overrided by the inherited classes
* @method render
* @param {CanvasRenderingContext2D} context the context into render the node
* */
render: function (context) {
//save current state
this.beforeRender(context);
//restore state
this.afterRender(context);
},
/**
* Empty ghost rendering function.
* Render here your custom nodes with a single color (cgsgGhostColor).
* This will be used by the SceneGraph to know if the mouse cursor is over this nodes.
*
* @method renderGhost
* @param ghostContext The context for the ghost rendering
*/
renderGhost: function (ghostContext) {
//save current state
this.beforeRenderGhost(ghostContext);
//restore state
this.afterRenderGhost(ghostContext);
},
/**
* Render the selection box and handle boxes around the bounding box of this node when selected
* @protected
* @method renderSelected
* @param {CanvasRenderingContext2D} context the context into render the node
* */
renderSelected: function (context) {
this._absolutePosition = this.getAbsolutePosition(false);
this._absoluteScale = this.getAbsoluteScale(false);
context.strokeStyle = this.selectionLineColor;
context.lineWidth = this.selectionLineWidth / this._absoluteScale.y;
context.beginPath();
//top line
context.moveTo(0, 0);
context.lineTo(this.dimension.width, 0);
//bottom line
context.moveTo(0, this.dimension.height);
context.lineTo(this.dimension.width, this.dimension.height);
context.stroke();
context.closePath();
context.lineWidth = this.selectionLineWidth / this._absoluteScale.x;
context.beginPath();
//left line
context.moveTo(0, 0);
context.lineTo(0, this.dimension.height);
//right line
context.moveTo(this.dimension.width, 0);
context.lineTo(this.dimension.width, this.dimension.height);
context.stroke();
context.closePath();
//draw the resize handles
if (this.isResizable) {
// draw the handle boxes
var halfX = this.selectionHandleSize / (2 * this._absoluteScale.x);
var halfY = this.selectionHandleSize / (2 * this._absoluteScale.y);
// 0 1 2
// 3 4
// 5 6 7
// top left, middle, right
this.resizeHandles[0].translateTo(-halfX, -halfY);
this.resizeHandles[1].translateTo(this.dimension.width / 2 - halfX, -halfY);
this.resizeHandles[2].translateTo(this.dimension.width - halfX, -halfY);
// middle left
this.resizeHandles[3].translateTo(-halfX, this.dimension.height / 2 - halfY);
// middle right
this.resizeHandles[4].translateTo(this.dimension.width - halfX,
this.dimension.height / 2 - halfY);
// bottom left, middle, right
this.resizeHandles[6].translateTo(this.dimension.width / 2 - halfX,
this.dimension.height - halfY);
this.resizeHandles[5].translateTo(-halfX, this.dimension.height - halfY);
this.resizeHandles[7].translateTo(this.dimension.width - halfX,
this.dimension.height - halfY);
for (var i = 0; i < 8; i++) {
this.resizeHandles[i].size = this.selectionHandleSize;
this.resizeHandles[i].fillColor = this.selectionHandleColor;
this.resizeHandles[i].strokeColor = this.selectionLineColor;
this.resizeHandles[i].lineWidth = this.selectionLineWidth;
this.resizeHandles[i].render(context);
}
}
},
/**
* Must be called before to start the rendering of the nodes
* @protected
* @method beforeRender
* @param {CanvasRenderingContext2D} context the context into render the nodes
* */
beforeRender: function (context) {
//first save the current context state
context.save();
//move the context to the nodes's relative position
context.translate(this.position.x, this.position.y);
context.scale(this.scale.x, this.scale.y);
// translate context to center of canvas
if (cgsgExist(this.rotationCenter)) {
context.translate(this.dimension.width * this.rotationCenter.x,
this.dimension.height * this.rotationCenter.y);
context.rotate(this.rotation.angle);
context.translate(-this.dimension.width * this.rotationCenter.x,
-this.dimension.height * this.rotationCenter.y);
}
else {
context.rotate(this.rotation.angle);
}
},
/**
* Must be called after a render
* @protected
* @method afterRender
* @param {CanvasRenderingContext2D} context the context into render the nodes
* */
afterRender: function (context) {
//render all children
if (!this.isALeaf()) {
//draw children
for (var i = 0, len = this.children.length; i < len; ++i) {
var childNode = this.children[i];
if (childNode.isVisible) {
childNode.render(context);
}
}
}
//restore the context state
context.restore();
},
/**
* Must be called before begin to render the nodes in GHOST mode
* @protected
* @method beforeRenderGhost
* @param {CanvasRenderingContext2D} context the context into render the nodes
*/
beforeRenderGhost: function (context) {
//first save the current context state
context.save();
//move the context to the nodes's relative position
context.translate(this._absolutePosition.x, this._absolutePosition.y);
context.rotate(this._absoluteRotation.angle);
context.scale(this._absoluteScale.x, this._absoluteScale.y);
},
/**
* Must be called before begin to render
* @protected
* @method afterRenderGhost
* @param {CanvasRenderingContext2D} context the context into render the nodes
* */
afterRenderGhost: function (context) {
//restore the context state
context.restore();
},
/**
* Mark this nodes as selected
* @method setSelected
* @param {Boolean} isSelected
* */
setSelected: function (isSelected) {
this.isSelected = isSelected;
this._isDrag = true;
if (isSelected && this.onSelect !== null) {
this.onSelect({node: this});
}
else if (this.onDeselect !== null) {
this.onDeselect({node: this});
}
},
/**
* return this if this nodes is under the mice cursor
* Can be overrided by inherited klass to optimize this perform.
* This default function used the ghost rendering method
* @protected
* @method detectSelection
* @param {CGSGPosition} mousePosition A CGSGPosition object
* @param {CanvasRenderingContext2D} ghostContext
* @param {CGSGScale} absoluteScale
*/
detectSelection: function (mousePosition, ghostContext, absoluteScale) {
if (this.pickNodeMethod == CGSGPickNodeMethod.REGION) {
if (mousePosition.x >= this._absolutePosition.x
&& mousePosition.x < this._absolutePosition.x + this.getWidth() * absoluteScale.x
&& mousePosition.y >= this._absolutePosition.y
&& mousePosition.y < this._absolutePosition.y + this.getHeight() * absoluteScale.y
) {
return this;
}
}
else /*if (this.pickNodeMethod == CGSGPickNodeMethod.GHOST)*/ {
// draw shape onto ghost context
this.renderGhost(ghostContext);
// get image data at the mouse x,y pixel
var imageData = ghostContext.getImageData(mousePosition.x, mousePosition.y, 1, 1);
cgsgClearContext(ghostContext);
// if the mouse pixel exists, select this nodes
if (imageData.data[0] != 0 || imageData.data[1] != 0 || imageData.data[2] != 0) {
return this;
}
}
return null;
},
/**
* return this if this nodes is under the region
* Can be overrided by inherited klass to optimize this perform.
* This default function used the ghost rendering method
* @protected
* @method detectSelectionInRegion
* @param {CGSGRegion} region The region to check
* @param {CanvasRenderingContext2D} ghostContext
* @param {CGSGScale} absoluteScale
*/
detectSelectionInRegion: function (region, ghostContext, absoluteScale) {
if (this.pickNodeMethod == CGSGPickNodeMethod.REGION) {
var us = this.getAbsoluteRegion();
/*var cond1 = region.position.x + region.dimension.width < us.position.x;
var cond2 = region.position.x > us.position.x + us.dimension.width;
var cond3 = region.position.y + region.dimension.height < us.position.y;
var cond4 = region.position.y > us.position.y + us.dimension.height;
if (!(cond1 || cond2 || cond3 || cond4)) {
return this;
} */
//select this node only if it is totally inside the region
if (cgsgRegionIsInRegion(us, region, 0)) {
return this;
}
}
else /*if (this.pickNodeMethod == CGSGPickNodeMethod.GHOST)*/ {
// draw shape onto ghost context
this.renderGhost(ghostContext);
// get image data at the mouse x,y pixel
var imageData = ghostContext.getImageData(region.position.x, region.position.y, region.dimension.width,
region.dimension.height);
cgsgClearContext(ghostContext);
// if the a pixel exists in the region then, select this node
for (var i = 0, len = imageData.length; i < len; ++i) {
if (imageData.data[i] != 0) {
return this;
}
}
}
return null;
},
/**
* Check if this nodes is under the cursor position.
* @public
* @method pickNode
* @param {CGSGPosition} mousePosition position of the mouse on the canvas
* @param {CGSGScale} absoluteScale a CGSGScale absolute relativeScale of all parents
* @param {CanvasRenderingContext2D} ghostContext a copy of the canvas context
* @param {Boolean} recursively if false, don't traverse the children of this nodes
* @param {Function} condition Condition to be picked
* ie: "color=='yellow'" or "classType=='CGSGNodeImage' && this.globalAlpha>0.5"
*/
pickNode: function (mousePosition, absoluteScale, ghostContext, recursively, condition) {
var selectedNode = null;
var childAbsoluteScale = null;
if (cgsgExist(absoluteScale)) {
childAbsoluteScale = absoluteScale.copy();
childAbsoluteScale.multiply(this.scale);
}
else {
childAbsoluteScale = this.getAbsoluteScale(false);
}
if (this.isTraversable && (this.isClickable || this.isResizable || this.isDraggable)) {
if (!cgsgExist(condition) || condition(this) === true) {
this.computeAbsoluteMatrix(false);
selectedNode =
this.detectSelection(mousePosition, ghostContext, childAbsoluteScale);
}
}
//traverse the children if asked
if (this.isTraversable && recursively && !this.isALeaf()) {
for (var i = this.children.length - 1; i >= 0; --i) {
var childNode = this.children[i];
var selectedChild = childNode.pickNode(mousePosition,
childAbsoluteScale, ghostContext,
recursively, condition);
if (selectedChild !== null && selectedChild !== undefined) {
selectedNode = selectedChild;
break;
}
}
childAbsoluteScale = null;
}
return selectedNode;
},
/**
* Return all nodes (Array) in the given region
* @public
* @method pickNodes
* @param {CGSGRegion} region of the canvas to check
* @param {CGSGScale} absoluteScale a CGSGScale absolute relativeScale of all parents
* @param {CanvasRenderingContext2D} ghostContext a copy of the canvas context
* @param {Boolean} recursively if false, don't traverse the children of this nodes
* @param {Function} condition Condition to be picked
* ie: "color=='yellow'" or "classType=='CGSGNodeImage' && this.globalAlpha>0.5"
*/
pickNodes: function (region, absoluteScale, ghostContext, recursively, condition) {
var selectedNodes = [];
var childAbsoluteScale = null;
if (cgsgExist(absoluteScale)) {
childAbsoluteScale = absoluteScale.copy();
childAbsoluteScale.multiply(this.scale);
}
else {
childAbsoluteScale = this.getAbsoluteScale(false);
}
if (this.isTraversable && (/*this.isClickable ||*/ this.isResizable || this.isDraggable)) {
if (!cgsgExist(condition) || condition(this) === true) {
this.computeAbsoluteMatrix(false);
var selected = this.detectSelectionInRegion(region, ghostContext, childAbsoluteScale);
if (cgsgExist(selected)) {
selectedNodes.push(selected);
}
}
}
//traverse the children if asked
if (this.isTraversable && recursively && !this.isALeaf()) {
for (var i = this.children.length - 1; i >= 0; --i) {
var childNode = this.children[i];
var selectedChildren = childNode.pickNodes(region,
childAbsoluteScale, ghostContext,
recursively, condition);
if (selectedChildren !== null && selectedChildren !== undefined) {
selectedNodes = selectedNodes.concat(selectedChildren);
}
}
childAbsoluteScale = null;
}
return selectedNodes;
},
/**
* Return true if this nodes has no child
* @method isALeaf
* */
isALeaf: function () {
return this.children.length <= 0;
},
//// TRANSFORMATION MANIPULATION //////
/**
* Replace current relative position by this new one
* @method translateTo
* @param {Number} newRelativeX
* @param {Number} newRelativeY
* @param {Boolean} computeAbsoluteValue (default: true)
*/
translateTo: function (newRelativeX, newRelativeY, computeAbsoluteValue) {
this.position.translateTo(newRelativeX, newRelativeY);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absolutePosition = this.getAbsolutePosition(true);
}
},
/**
* Add new coordinate to the current relative one
* @method translateWith
* @param {Number} x
* @param {Number} y
* @param {Boolean} computeAbsoluteValue (default: true)
* */
translateWith: function (x, y, computeAbsoluteValue) {
this.position.translateWith(x, y);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absolutePosition = this.getAbsolutePosition(true);
}
},
/**
* Add new coordinate to the current relative one
* @method translateBy
* @param {Number} x
* @param {Number} y
* @param {Boolean} computeAbsoluteValue (default: true)
* */
translateBy: function (x, y, computeAbsoluteValue) {
this.position.translateBy(x, y);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absolutePosition = this.getAbsolutePosition(true);
}
},
/**
* Replace current dimension by these new ones
* @method resizeTo
* @param {Number} newWidth
* @param {Number} newHeight
* */
resizeTo: function (newWidth, newHeight) {
this.dimension.resizeTo(newWidth, newHeight);
},
/**
* Multiply current dimension by these new ones
* @method resizeBy
* @param {Number} widthFactor
* @param {Number} heightFactor
* */
resizeBy: function (widthFactor, heightFactor) {
this.dimension.resizeBy(widthFactor, heightFactor);
},
/**
* Increase/decrease current dimension with adding values
* @method resizeWith
* @param {Number} width
* @param {Number} height
* */
resizeWith: function (width, height) {
this.dimension.resizeWith(width, height);
},
/**
* Replace current relative relativeScale by this new one
* @method scaleTo
* @param {Number} scaleX
* @param {Number} scaleY
* @param {Boolean} computeAbsoluteValue (default: true)
* */
scaleTo: function (scaleX, scaleY, computeAbsoluteValue) {
this.scale.x = scaleX;
this.scale.y = scaleY;
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteScale = this.getAbsoluteScale(true);
}
},
/**
* Multiply this relativeScale factor by the current relative relativeScale
* @method scaleBy
* @param {Number} scaleFactorX
* @param {Number} scaleFactorY
* @param {Boolean} computeAbsoluteValue (default: true)
* */
scaleBy: function (scaleFactorX, scaleFactorY, computeAbsoluteValue) {
this.scale.x *= scaleFactorX;
this.scale.y *= scaleFactorY;
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteScale = this.getAbsoluteScale(true);
}
},
/**
* Add to the current relative Scale
* @method scaleWith
* @param {Number} x
* @param {Number} y
* @param {Boolean} computeAbsoluteValue (default: true)
* */
scaleWith: function (x, y, computeAbsoluteValue) {
this.scale.x += x;
this.scale.y += y;
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteScale = this.getAbsoluteScale(true);
}
},
/**
* Replace current relative relativeRotation by this new oneScale
* @method rotateTo
* @param {Number} newAngle
* @param {Boolean} computeAbsoluteValue (default: true)
*
* */
rotateTo: function (newAngle, computeAbsoluteValue) {
this.rotation.rotateTo(newAngle);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteRotation = this.getAbsoluteRotation(true);
}
},
/**
* Multiply this relativeScale factor by the current relative relativeScale
* @method rotateBy
* @param {Number} rotateFactor
* @param {Boolean} computeAbsoluteValue (default: true)
* */
rotateBy: function (rotateFactor, computeAbsoluteValue) {
this.rotation.rotateBy(rotateFactor);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteRotation = this.getAbsoluteRotation(true);
}
},
/**
* Add this angle to the current relative relativeRotation
* @method rotateWith
* @param {Number} angle
* @param {Boolean} computeAbsoluteValue (default: true)
* */
rotateWith: function (angle, computeAbsoluteValue) {
this.rotation.rotateWith(angle);
if (this.needToKeepAbsoluteMatrix && computeAbsoluteValue !== false) {
this._absoluteRotation = this.getAbsoluteRotation(true);
}
},
//// CHILDREN MANIPULATION //////
/**
* Add a new nodes into this one, at the end of the list
* @method addChild
* @param {CGSGNode} newNode the nodes to add as a child
* */
addChild: function (newNode) {
newNode._parentNode = this;
this.children[this.children.length] = newNode;
},
/**
* Add a new nodes at a particular index in the list of children.
* If the index is too large, the nodes will be inserted at the end of the list
* @method addChildAt
* @param {CGSGNode} newNode the nodes to insert as a child
* @param {Number} index the position of the new child in the list
* */
addChildAt: function (newNode, index) {
if (index > this.children.length) {
index = this.children.length;
}
for (var i = this.children.length; i >= index; --i) {
this.children[i] = this.children[i - 1];
}
newNode._parentNode = this;
this.children[index] = newNode;
},
/**
* Remove the child passed in parameter and delete it
* @method removeChild
* @param {CGSGNode} node the nodes to remove
* @param {Boolean} searchRecursively if true, search the nodes on all the tree from this nodes
* @return {Boolean} true if the child was correctly removed or false if the nodes was not found.
* */
removeChild: function (node, searchRecursively) {
var index = this.children.indexOf(node);
if (index >= 0) {
this.children.without(node);
node.free();
return true;
}
if (searchRecursively) {
for (var i = 0, len = this.children.length; i < len; ++i) {
var childNode = this.children[i];
if (childNode.removeChild(node, true)) {
return true;
}
}
}
return false;
},
/**
* remove all children, delete them and reset the current parameters
* @method removeAll
* */
removeAll: function () {
/*for (var i = this.children.length; i >=0; --i) {
var childNode = this.children[i];
childNode.removeAll();
this.removeChild(childNode, true);
}*/
this.free();
},
/**
* Detach the nodes in index 'index' without delete it. So it's not a child anymore
* @method detachChildAt
* @param {Number} index
*/
detachChildAt: function (index) {
if (index >= 0 && index < this.children.length) {
this.detachChild(this.children[index]);
}
},
/**
* Detach the nodes without delete it. So it's not a child anymore
* @method detachChild
* @param {CGSGNode} childNode
*/
detachChild: function (childNode) {
if (cgsgExist(childNode)) {
childNode._parentNode = null;
/*this.children = */
this.children.without(childNode);
}
},
/**
* Execute/Eval the script passed in parameter in "this" scope.
* Used to set new value to an attribute of a node
* @method evalSet
* @param {String} attribute The attribute to be changed
* @param {*} value The new value for the attribute
*
* @example node.evalSet("position.y", 12);
*/
evalSet: function (attribute, value) {
//check for common properties to optimize performances
if (attribute == "position.x") {
this.translateTo(value, this.position.y, this.needToKeepAbsoluteMatrix);
}
else if (attribute == "position.y") {
this.translateTo(this.position.x, value, this.needToKeepAbsoluteMatrix);
}
else if (attribute == "dimension.width") {
this.resizeTo(value, this.dimension.height);
}
else if (attribute == "dimension.height") {
this.resizeTo(this.dimension.width, value);
}
else if (attribute == "scale.x") {
this.scaleTo(value, this.scale.y, this.needToKeepAbsoluteMatrix);
}
else if (attribute == "scale.y") {
this.scaleTo(this.scale.x, value, this.needToKeepAbsoluteMatrix);
}
else if (attribute == "rotation" || attribute == "rotation.angle") {
this.rotateTo(value, this.needToKeepAbsoluteMatrix);
}
else if (attribute == "globalAlpha") {
this.globalAlpha = value;
}
else if (attribute == "isVisible") {
this.isVisible = value;
}
else if (attribute == "rotationCenter.x") {
this.rotationCenter.x = value;
}
else if (attribute == "rotationCenter.y") {
this.rotationCenter.y = value;
}
else if (attribute == "color.r") {
var rgb = CGSGColor.hex2rgb(this.color);
this.color = CGSGColor.rgb2hex(value, rgb.g, rgb.b);
}
else if (attribute == "color.g") {
var rgb = CGSGColor.hex2rgb(this.color);
this.color = CGSGColor.rgb2hex(rgb.r, value, rgb.b);
}
else if (attribute == "color.b") {
var rgb = CGSGColor.hex2rgb(this.color);
this.color = CGSGColor.rgb2hex(rgb.r, rgb.g, value);
}
//generic property
else {
eval("this." + attribute + "=" + value);
}
/*if (this.needToKeepAbsoluteMatrix) {
if (attribute.indexOf("position") == 0) {
this._absolutePosition = this.getAbsolutePosition(true);
}
else if (attribute.indexOf("rotation") == 0) {
this._absoluteRotation = this.getAbsoluteRotation(true);
}
else if (attribute.indexOf("scale") == 0) {
this._absoluteScale = this.getAbsoluteScale(true);
}
}*/
},
/**
* Set the region inside which one this node ca be placed an can move
* @public
* @method setRegionConstraint
* @param {CGSGRegion} region a CGSGRegion relatively to this parent region. Can be null.
*/
setRegionConstraint: function (region) {
this.regionConstraint = region;
},
/**
* @public
* @method getAbsolutePosition
* @return {CGSGPosition} the absolute positions of this node
*/
getAbsolutePosition: function (recursive) {
var n = this;
var translation = this.position.copy();
while (n._parentNode !== null) {
translation.multiply(n._parentNode.scale);
n = n._parentNode;
}
if (this._parentNode !== null) {
translation.add(this._parentNode.getAbsolutePosition(false));
}
if (recursive !== false) {
for (var c = 0; c < this.children.length; c++) {
if (cgsgExist(this.children[c])) {
this.children[c]._absolutePosition = this.children[c].getAbsolutePosition(recursive);
}
}
}
return translation;
},
/**
* @public
* @method getAbsoluteScale
* @return {CGSGScale} the absolute scale of this node
*/
getAbsoluteScale: function (recursive) {
var n = this;
var s = this.scale.copy();
while (n._parentNode !== null) {
s.multiply(n._parentNode.scale);
n = n._parentNode;
}
if (recursive !== false) {
for (var c = 0; c < this.children.length; c++) {
if (cgsgExist(this.children[c])) {
this.children[c]._absoluteScale = this.children[c].getAbsoluteScale(recursive);
}
}
}
return s;
},
/**
* @public
* @method getAbsoluteRotation
* @return {CGSGRotation} the absolute rotation of this node
*/
getAbsoluteRotation: function (recursive) {
var n = this;
var r = this.rotation.copy();
while (n._parentNode !== null) {
r.add(n._parentNode.rotation.angle);
n = n._parentNode;
}
if (recursive !== false) {
for (var c = 0; c < this.children.length; c++) {
if (cgsgExist(this.children[c])) {
this.children[c]._absoluteRotation = this.children[c].getAbsoluteRotation(recursive);
}
}
}
return r;
},
/**
* Compute the absolute position, rotation and scale in the canvas container
* @public
* @method computeAbsoluteMatrix
* @param {Boolean} recursive if !== false, compute recursively
* */
computeAbsoluteMatrix: function (recursive) {
this._absolutePosition = this.getAbsolutePosition(false);
this._absoluteScale = this.getAbsoluteScale(false);
this._absoluteRotation = this.getAbsoluteRotation(false);
if (recursive !== false) {
for (var c = 0; c < this.children.length; c++) {
if (cgsgExist(this.children[c])) {
this.children[c].computeAbsoluteMatrix(recursive);
}
}
}
},
/**
* @method getAbsoluteLeft
* @return {Number}
*/
getAbsoluteLeft : function () {
return this._absolutePosition.x;
},
/**
* @method getAbsoluteRight
* @return {Number}
*/
getAbsoluteRight : function () {
return this._absolutePosition.x + this.getAbsoluteWidth();
},
/**
* @method getAbsoluteTop
* @return {Number}
*/
getAbsoluteTop : function () {
return this._absolutePosition.y;
},
/**
* @method getAbsoluteBottom
* @return {Number}
*/
getAbsoluteBottom: function () {
return this._absolutePosition.y + this.getAbsoluteHeight();
},
/**
* @method getAbsoluteWidth
* @return {Number}
*/
getAbsoluteWidth : function () {
return this.getWidth() * this._absoluteScale.x;
},
/**
* @method getAbsoluteHeight
* @return {Number}
*/
getAbsoluteHeight: function () {
return this.getHeight() * this._absoluteScale.y;
},
/**
* @method getWidth
* @return {Number}
*/
getWidth : function () {
return this.dimension.width;
},
/**
* @method getHeight
* @return {Number}
*/
getHeight : function () {
return this.dimension.height;
},
/**
* Test if this node is colliding the node in parameter. Don't forget to add nodes to CGSGCollisionManager.
*
* @public
* @method isColliding
* @return {Boolean} true if the 2 nodes are colliding. They are colliding if the distance between them is minus than the threshold parameter
* @param {CGSGNode} node a CGSGNode
* @param {Number} threshold space between the 2 nodes before considering they are colliding (in mode region)
*/
isColliding: function (node, threshold) {
return cgsgCollisionManager.isColliding(this, node, threshold);
},
/**
* @public
* @method getListOfCollidingBrothers
* @return {Array} a Array of nodes this one is colliding with (can be empty)
* @param {Number} threshold space between the 2 nodes before considering they are colliding
*/
getListOfCollidingBrothers: function (threshold) {
var listOfCollidingNodes = [];
var brother = null;
for (var n = 0; n < this._parentNode.children.length; n++) {
brother = this._parentNode.children[n];
if (brother !== this && this.isColliding(brother, threshold)) {
listOfCollidingNodes.push(brother);
}
}
return listOfCollidingNodes;
},
/**
* @public
* @method isCollidingABrother
* @param {Number} threshold space between the 2 nodes before considering they are colliding
* @return {Boolean} true if this node is colliding one of the other children of its parent node
*/
isCollidingABrother: function (threshold) {
var brother = null;
for (var n = 0; n < this._parentNode.children.length; n++) {
brother = this._parentNode.children[n];
if (brother !== this && this.isColliding(brother, threshold)) {
return true;
}
}
return false;
},
/*
* TODO : to be completed
* Return the list of lines going joigning nodes' peaks
* param onlyBrothers a Boolean. Default = true
* param threshold distance from which the detectection is done
* return an array of CGSGVector2D (can be empty)
*/
/*getMagneticLines : function(onlyBrothers, threshold) {
if (!cgsgExist(onlyBrothers)) {
onlyBrothers = true;
}
//compute vectors
var topVector = this.getAbsoluteTop();
var bottomVector = this.getAbsoluteBottom();
var leftVector = this.getAbsoluteLeft();
var rightVector = this.getAbsoluteRight();
//line = a point and a normalized CGSGVector2D (ie : [0, 1] or [1, 0])
var listOfLines = [];
var brother = null;
for (var n = 0; n < this._parentNode.children.length; n++) {
brother = this._parentNode.children[n];
//vectors v & v' are colinear if and only if xy’ - yx’ = 0.
}
return listOfLines;
},*/
/**
* Must be overrided by inherited classes
* @method copy
* @param {CGSGNode} node
* @return {CGSGNode} a copy of this node
*/
copy: function (node) {
if (node === null || node === undefined) {
node = new CGSGNode(this.position.x, this.position.y, this.dimension.width, this.dimension.height);
}
node.classType = this.classType;
node.name = this.name;
node.globalAlpha = this.globalAlpha;
node.isVisible = this.isVisible;
node.isProportionalResize = this.isProportionalResize;
node.pickNodeMethod = this.pickNodeMethod;
node.needToKeepAbsoluteMatrix = this.needToKeepAbsoluteMatrix;
//list of the children (empty if this nodes is a leaf)
//this.children = [];
if (this.regionConstraint !== null) {
node.regionConstraint = this.regionConstraint.copy();
}
//can be fulfilled by the developer to put in whatever he needs
if (this.userdata !== null) {
node.userdata = this.userdata.copy();
}
//selection attributes
//if true, this nodes is clickable and so will be checked by the pickNode function
node.isClickable = this.isClickable;
//if true, this nodes can be selected and so can be transformed (dimension)
node.isResizable = this.isResizable;
node.isDraggable = this.isDraggable;
node.isTraversable = this.isTraversable;
node.selectionLineColor = this.selectionLineColor;
node.selectionLineWidth = this.selectionLineWidth;
node.selectionHandleSize = this.selectionHandleSize;
node.selectionHandleColor = this.selectionHandleColor;
node._id = this._id;
node._ghostColor = cgsgGhostColor;
node.translateTo(this.position.x, this.position.y);
node.resizeTo(this.dimension.width, this.dimension.height);
node.scaleTo(this.scale.x, this.scale.y);
node.rotateTo(this.rotation.angle);
node.selectableZone =
new CGSGRegion(node.position.x, node.position.y, node.dimension.width, node.dimension.height);
//all the events for the node
node.onMouseOver = this.onMouseOver;
node.onMouseOut = this.onMouseOut;
node.onClick = this.onClick;
node.onDblClick = this.onDblClick;
node.onDrag = this.onDrag;
node.onDragEnd = this.onDragEnd;
node.onResize = this.onResize;
node.onResizeEnd = this.onResizeEnd;
node.onSelect = this.onSelect;
node.onDeselect = this.onDeselect;
node.computeAbsoluteMatrix(true);
return node;
},
/**
* free memory taken by this object and it's children.
* The 'userData' property won't be freed
* @method free
*/
free: function () {
for (var c = this.children.length - 1; c >= 0; c--) {
this.children[c].free();
}
this.children.clear();
cgsgFree(this);
}
}
);