Prechádzať zdrojové kódy

Adding Actions Builder with the README.md file

luaacro 10 rokov pred
rodič
commit
5849f66fb3
49 zmenil súbory, kde vykonal 54845 pridanie a 0 odobranie
  1. 42 0
      Tools/ActionsBuilder/README.md
  2. 86 0
      Tools/ActionsBuilder/Sources/actionsbuilder.actionNode.js
  3. 118 0
      Tools/ActionsBuilder/Sources/actionsbuilder.contextMenu.js
  4. 351 0
      Tools/ActionsBuilder/Sources/actionsbuilder.js
  5. 251 0
      Tools/ActionsBuilder/Sources/actionsbuilder.list.js
  6. 98 0
      Tools/ActionsBuilder/Sources/actionsbuilder.main.js
  7. 2551 0
      Tools/ActionsBuilder/Sources/actionsbuilder.max.js
  8. 483 0
      Tools/ActionsBuilder/Sources/actionsbuilder.parameters.js
  9. 85 0
      Tools/ActionsBuilder/Sources/actionsbuilder.toolbar.js
  10. 433 0
      Tools/ActionsBuilder/Sources/actionsbuilder.utils.js
  11. 646 0
      Tools/ActionsBuilder/Sources/actionsbuilder.viewer.js
  12. 34265 0
      Tools/ActionsBuilder/Sources/babylon.max.js
  13. 36 0
      Tools/ActionsBuilder/Sources/fonts.css
  14. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.eot
  15. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.otf
  16. 2984 0
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.svg
  17. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.ttf
  18. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.woff
  19. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.eot
  20. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.otf
  21. 2997 0
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.svg
  22. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.ttf
  23. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.woff
  24. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.eot
  25. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.otf
  26. 2990 0
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.svg
  27. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.ttf
  28. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.woff
  29. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.eot
  30. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.otf
  31. 3016 0
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.svg
  32. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.ttf
  33. BIN
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.woff
  34. 91 0
      Tools/ActionsBuilder/Sources/index-debug.html
  35. 191 0
      Tools/ActionsBuilder/Sources/index.css
  36. 72 0
      Tools/ActionsBuilder/Sources/index.html
  37. 11 0
      Tools/ActionsBuilder/Sources/raphael.js
  38. 101 0
      Tools/ActionsBuilder/actionsbuilder.actionNode.ts
  39. 149 0
      Tools/ActionsBuilder/actionsbuilder.contextMenu.ts
  40. 301 0
      Tools/ActionsBuilder/actionsbuilder.list.ts
  41. 105 0
      Tools/ActionsBuilder/actionsbuilder.main.ts
  42. 551 0
      Tools/ActionsBuilder/actionsbuilder.parameters.ts
  43. 94 0
      Tools/ActionsBuilder/actionsbuilder.toolbar.ts
  44. 326 0
      Tools/ActionsBuilder/actionsbuilder.ts
  45. 527 0
      Tools/ActionsBuilder/actionsbuilder.utils.ts
  46. 772 0
      Tools/ActionsBuilder/actionsbuilder.viewer.ts
  47. 42 0
      Tools/ActionsBuilder/gulpfile.js
  48. 11 0
      Tools/ActionsBuilder/package.json
  49. 69 0
      Tools/ActionsBuilder/raphaeljs.d.ts

+ 42 - 0
Tools/ActionsBuilder/README.md

@@ -0,0 +1,42 @@
+Babylon.js Actions Builder
+==========
+
+The Actions Builder is a part of the 3ds Max plugin ([Max2Babylon](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/3ds%20Max))
+that allows to build actions without any line of code.
+
+Tutorial and informations about the Actions Builder [here](https://medium.com/babylon-js/actions-builder-b05e72aa541a)
+
+Built files are located in "./Sources"
+
+# Install and Build files
+
+### Install dependencies:
+```
+npm install
+```
+
+### Build Actions Builder:
+```
+gulp
+```
+
+### Build JS files from their respective TS files:
+```
+gulp debug
+```
+
+### Build on save:
+```
+gulp watch
+```
+
+### Update gulpfile.js to add your own files:
+```
+var files = [
+    // Files
+    ...
+    "file1.js",
+    "file2.js",
+    ...
+];
+```

+ 86 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.actionNode.js

@@ -0,0 +1,86 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var Node = (function () {
+        function Node() {
+            this.rect = null;
+            this.text = null;
+            this.line = null;
+            this.detached = false;
+            this.minimized = false;
+        }
+        /**
+        * Returns if the point (x, y) is inside the text or rect
+        * @param x: the x position of the point
+        * @param y: the y position of the point
+        */
+        Node.prototype.isPointInside = function (x, y) {
+            return this.rect.isPointInside(x, y) || this.text.isPointInside(x, y);
+        };
+        return Node;
+    })();
+    ActionsBuilder.Node = Node;
+    var Action = (function () {
+        /**
+        * Constructor
+        * @param node: The associated node to draw in the viewer
+        */
+        function Action(node) {
+            this.parent = null;
+            this.children = new Array();
+            this.name = "";
+            this.type = ActionsBuilder.Type.OBJECT;
+            this.properties = new Array();
+            this.propertiesResults = new Array();
+            this.combineArray = null;
+            this.hub = null;
+            this.combineAction = null;
+            this.node = node;
+        }
+        /*
+        * Removes a combined action from the combine array
+        * @param action: the action to remove
+        */
+        Action.prototype.removeCombinedAction = function (action) {
+            if (action === null || this.combineArray === null) {
+                return false;
+            }
+            var index = this.combineArray.indexOf(action);
+            if (index !== -1) {
+                this.combineArray.splice(index, 1);
+            }
+            return false;
+        };
+        /*
+        * Adds a child
+        * @param child: the action to add as child
+        */
+        Action.prototype.addChild = function (child) {
+            if (child === null) {
+                return false;
+            }
+            this.children.push(child);
+            child.parent = this;
+            return true;
+        };
+        /*
+        * Removes the given action to children
+        * @param child: the child to remove
+        */
+        Action.prototype.removeChild = function (child) {
+            var indice = this.children.indexOf(child);
+            if (indice !== -1) {
+                this.children.splice(indice, 1);
+                return true;
+            }
+            return false;
+        };
+        /*
+        * Clears the children's array
+        */
+        Action.prototype.clearChildren = function () {
+            this.children = new Array();
+        };
+        return Action;
+    })();
+    ActionsBuilder.Action = Action;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 118 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.contextMenu.js

@@ -0,0 +1,118 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var ContextMenu = (function () {
+        /*
+        * Constructor
+        * @param viewer: the graph viewer
+        */
+        function ContextMenu(viewer) {
+            this.showing = false;
+            this.savedColor = Raphael.rgb(255, 255, 255);
+            this.overColor = Raphael.rgb(140, 200, 230);
+            this._viewer = null;
+            this.elements = [
+                { text: "Reduce", node: null, action: "onReduce" },
+                { text: "Delete", node: null, action: "onRemoveNode" },
+                { text: "Delete branch", node: null, action: "onRemoveBranch" },
+                { text: "Connect / Disconnect", node: null, action: "onDetachAction" },
+                { text: "Copy", node: null, action: "onCopyStructure" },
+                { text: "Paste", node: null, action: "onPasteStructure" },
+                // Add other elements here
+                { text: "", node: null, action: null } // Color separator (top)
+            ];
+            // Members
+            this._viewer = viewer;
+            // Configure
+            this.attachControl(this._viewer.paper.canvas);
+        }
+        ContextMenu.prototype.attachControl = function (element) {
+            var _this = this;
+            var onClick = function (event) {
+                var x = _this._viewer.mousex;
+                var y = _this._viewer.mousey;
+                // Remove all context menu nodes, and run action if selected
+                if (_this.showing) {
+                    for (var i = 0; i < _this.elements.length; i++) {
+                        var element = _this.elements[i];
+                        if (element.action && element.node.rect.isPointInside(x, y)) {
+                            _this._viewer.utils[element.action]();
+                            _this._viewer.update();
+                        }
+                        element.node.rect.remove();
+                        element.node.text.remove();
+                    }
+                }
+                _this.showing = false;
+            };
+            var onMouseMove = function (event) {
+                // Override context menu's node color if mouse is inside
+                if (_this.showing) {
+                    for (var i = 0; i < _this.elements.length; i++) {
+                        var element = _this.elements[i];
+                        if (element.text === "")
+                            continue;
+                        var x = _this._viewer.mousex;
+                        var y = _this._viewer.mousey;
+                        if (element.node.rect.isPointInside(x, y)) {
+                            element.node.rect.attr("fill", _this.overColor);
+                        }
+                        else {
+                            element.node.rect.attr("fill", _this.savedColor);
+                        }
+                    }
+                }
+            };
+            var onRightClick = function (event) {
+                var x = _this._viewer.mousex;
+                var y = _this._viewer.mousey;
+                _this._viewer.onClick(event);
+                // Set selected node
+                var result = _this._viewer.traverseGraph(null, x, y, true);
+                if (result.hit) {
+                }
+                // Properly draw the context menu on the screen
+                if (y + (ActionsBuilder.Viewer.NODE_HEIGHT * _this.elements.length) > _this._viewer.viewerElement.offsetHeight + _this._viewer.viewerElement.scrollTop) {
+                    y = (ActionsBuilder.Viewer.NODE_HEIGHT * _this.elements.length);
+                }
+                if (x + ActionsBuilder.Viewer.NODE_WIDTH > _this._viewer.viewerElement.offsetWidth + _this._viewer.viewerElement.scrollLeft) {
+                    x -= ActionsBuilder.Viewer.NODE_WIDTH;
+                }
+                if (!_this.showing) {
+                    if (_this._viewer.selectedNode === null)
+                        return;
+                    // Create elements
+                    var yOffset = 10;
+                    for (var i = 0; i < _this.elements.length - 1; i++) {
+                        var element = _this.elements[i];
+                        element.node = _this._viewer._createNode(element.text, ActionsBuilder.Type.OBJECT, true);
+                        element.node.rect.attr("fill", Raphael.rgb(216, 216, 216));
+                        element.node.rect.attr("x", x);
+                        element.node.rect.attr("y", y + yOffset);
+                        element.node.text.attr("x", x + 5);
+                        element.node.text.attr("y", y + yOffset + element.node.rect.attr("height") / 2);
+                        yOffset += ActionsBuilder.Viewer.NODE_HEIGHT;
+                    }
+                    // Color separator
+                    var separator = _this.elements[_this.elements.length - 1];
+                    separator.node = _this._viewer._createNode("", ActionsBuilder.Type.OBJECT, true);
+                    separator.node.rect.attr("fill", _this._viewer.getNodeColor(_this._viewer.selectedNode.type, _this._viewer.selectedNode.node.detached));
+                    separator.node.rect.attr("x", x);
+                    separator.node.rect.attr("y", y);
+                    separator.node.rect.attr("height", 10);
+                    // Finish
+                    _this.showing = true;
+                }
+                else {
+                    onClick(event);
+                    onRightClick(event);
+                }
+                window.event.returnValue = false;
+            };
+            document.addEventListener("click", onClick);
+            document.addEventListener("mousemove", onMouseMove);
+            element.addEventListener("contextmenu", onRightClick);
+        };
+        return ContextMenu;
+    })();
+    ActionsBuilder.ContextMenu = ContextMenu;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 351 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.js

@@ -0,0 +1,351 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    /**
+    * Defines static types
+    */
+    var Type = (function () {
+        function Type() {
+        }
+        Object.defineProperty(Type, "TRIGGER", {
+            get: function () {
+                return Type._TRIGGER;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Type, "ACTION", {
+            get: function () {
+                return Type._ACTION;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Type, "FLOW_CONTROL", {
+            get: function () {
+                return Type._FLOW_CONTROL;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Type, "OBJECT", {
+            get: function () {
+                return Type._OBJECT;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Type, "SCENE", {
+            get: function () {
+                return Type._SCENE;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Type._TRIGGER = 0;
+        Type._ACTION = 1;
+        Type._FLOW_CONTROL = 2;
+        Type._OBJECT = 3;
+        Type._SCENE = 4;
+        return Type;
+    })();
+    ActionsBuilder.Type = Type;
+    /*
+    * Defines the BABYLON.JS elements
+    */
+    var SceneElements = (function () {
+        function SceneElements() {
+        }
+        Object.defineProperty(SceneElements, "ENGINE", {
+            get: function () {
+                return SceneElements._ENGINE;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "SCENE", {
+            get: function () {
+                return SceneElements._SCENE;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "MESH", {
+            get: function () {
+                return SceneElements._MESH;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "LIGHT", {
+            get: function () {
+                return SceneElements._LIGHT;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "CAMERA", {
+            get: function () {
+                return SceneElements._CAMERA;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "MESHES", {
+            get: function () {
+                return SceneElements._MESHES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "LIGHTS", {
+            get: function () {
+                return SceneElements._LIGHTS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "CAMERAS", {
+            get: function () {
+                return SceneElements._CAMERAS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "SOUNDS", {
+            get: function () {
+                return SceneElements._SOUNDS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "MESH_PROPERTIES", {
+            get: function () {
+                return SceneElements._MESH_PROPERTIES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "LIGHT_PROPERTIES", {
+            get: function () {
+                return SceneElements._LIGHT_PROPERTIES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "CAMERA_PROPERTIES", {
+            get: function () {
+                return SceneElements._CAMERA_PROPERTIES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "SCENE_PROPERTIES", {
+            get: function () {
+                return SceneElements._SCENE_PROPERTIES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "TYPES", {
+            get: function () {
+                return SceneElements._TYPES;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(SceneElements, "OPERATORS", {
+            get: function () {
+                return SceneElements._OPERATORS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /*
+        * Methods
+        */
+        SceneElements.GetInstanceOf = function (object) {
+            if (object === null || object === undefined) {
+                return "";
+            }
+            return object.constructor.toString().match(/function (\w*)/)[1];
+        };
+        SceneElements.TestInstanceOf = function (object, propertyName) {
+            if (object === null || object.constructor === null) {
+                return false;
+            }
+            if (propertyName.length > 0 && propertyName[0] === "_")
+                return false;
+            var name = SceneElements.GetInstanceOf(object);
+            for (var i = 0; i < SceneElements.TYPES.length; i++) {
+                if (name === SceneElements.TYPES[i]) {
+                    return true;
+                }
+            }
+            return false;
+        };
+        /*
+        * BabylonJS objects
+        */
+        SceneElements._ENGINE = new BABYLON.Engine(document.getElementById("RenderCanvasID"));
+        SceneElements._SCENE = new BABYLON.Scene(SceneElements.ENGINE);
+        SceneElements._MESH = new BABYLON.Mesh("mesh", SceneElements._SCENE);
+        SceneElements._LIGHT = new BABYLON.Light("light", SceneElements._SCENE);
+        SceneElements._CAMERA = new BABYLON.Camera("camera", BABYLON.Vector3.Zero(), SceneElements._SCENE);
+        /*
+        * Objects names
+        */
+        SceneElements._MESHES = new Array();
+        SceneElements._LIGHTS = new Array();
+        SceneElements._CAMERAS = new Array();
+        SceneElements._SOUNDS = new Array();
+        /*
+        * Properties
+        */
+        SceneElements._MESH_PROPERTIES = new Array();
+        SceneElements._LIGHT_PROPERTIES = new Array();
+        SceneElements._CAMERA_PROPERTIES = new Array();
+        SceneElements._SCENE_PROPERTIES = new Array();
+        /*
+        * Types
+        */
+        SceneElements._TYPES = new Array();
+        /*
+        * Operators
+        */
+        SceneElements._OPERATORS = new Array();
+        return SceneElements;
+    })();
+    ActionsBuilder.SceneElements = SceneElements;
+    // Functions
+    var specialTypes = [
+        "StandardMaterial"
+    ];
+    SceneElements.MESH.material = new BABYLON.StandardMaterial("material", SceneElements.SCENE);
+    var addSpecialType = function (object, properties, thing) {
+        for (var specialThing in object[thing]) {
+            if (object[thing].hasOwnProperty(specialThing) && SceneElements.TestInstanceOf(object[thing][specialThing], specialThing)) {
+                properties.push(thing + "." + specialThing);
+            }
+        }
+    };
+    // Configure types
+    SceneElements.TYPES.push("Color3");
+    SceneElements.TYPES.push("Boolean");
+    SceneElements.TYPES.push("Number");
+    SceneElements.TYPES.push("Vector2");
+    SceneElements.TYPES.push("Vector3");
+    SceneElements.TYPES.push("String");
+    // Configure operators
+    SceneElements.OPERATORS.push("IsEqual");
+    SceneElements.OPERATORS.push("IsDifferent");
+    SceneElements.OPERATORS.push("IsGreater");
+    SceneElements.OPERATORS.push("IsLesser");
+    // Configure properties
+    for (var thing in SceneElements.MESH) {
+        var instance = SceneElements.GetInstanceOf(SceneElements.MESH[thing]);
+        if (SceneElements.MESH.hasOwnProperty(thing)) {
+            if (specialTypes.indexOf(instance) !== -1) {
+                addSpecialType(SceneElements.MESH, SceneElements.MESH_PROPERTIES, thing);
+            }
+            else if (SceneElements.TestInstanceOf(SceneElements.MESH[thing], thing)) {
+                SceneElements.MESH_PROPERTIES.push(thing);
+            }
+        }
+    }
+    for (var thing in SceneElements.LIGHT) {
+        if (SceneElements.LIGHT.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.LIGHT[thing], thing)) {
+            SceneElements.LIGHT_PROPERTIES.push(thing);
+        }
+    }
+    for (var thing in SceneElements.CAMERA) {
+        if (SceneElements.CAMERA.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.CAMERA[thing], thing)) {
+            SceneElements.CAMERA_PROPERTIES.push(thing);
+        }
+    }
+    for (var thing in SceneElements.SCENE) {
+        if (SceneElements.SCENE.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.SCENE[thing], thing)) {
+            SceneElements.SCENE_PROPERTIES.push(thing);
+        }
+    }
+    /**
+    * Actions Builder elements (triggers, actions & flow controls) that are
+    * arrays of Element
+    */
+    var Elements = (function () {
+        function Elements() {
+        }
+        Object.defineProperty(Elements, "TRIGGERS", {
+            get: function () {
+                return Elements._TRIGGERS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Elements, "ACTIONS", {
+            get: function () {
+                return Elements._ACTIONS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Elements, "FLOW_CONTROLS", {
+            get: function () {
+                return Elements._FLOW_CONTROLS;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Elements.GetElementFromName = function (name) {
+            for (var i = 0; i < Elements.TRIGGERS.length; i++) {
+                if (Elements.TRIGGERS[i].name === name) {
+                    return Elements._TRIGGERS[i];
+                }
+            }
+            for (var i = 0; i < Elements.ACTIONS.length; i++) {
+                if (Elements.ACTIONS[i].name === name) {
+                    return Elements._ACTIONS[i];
+                }
+            }
+            for (var i = 0; i < Elements.FLOW_CONTROLS.length; i++) {
+                if (Elements.FLOW_CONTROLS[i].name === name) {
+                    return Elements._FLOW_CONTROLS[i];
+                }
+            }
+            return null;
+        };
+        Elements._TRIGGERS = new Array();
+        Elements._ACTIONS = new Array();
+        Elements._FLOW_CONTROLS = new Array();
+        return Elements;
+    })();
+    ActionsBuilder.Elements = Elements;
+    // Configure triggers
+    Elements.TRIGGERS.push({ name: "OnPickTrigger", text: "pick", properties: [], description: "When the user picks the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnLeftPickTrigger", text: "left pick", properties: [], description: "When the user picks the edited mesh using the left click" });
+    Elements.TRIGGERS.push({ name: "OnRightPickTrigger", text: "right pick", properties: [], description: "When the user picks the edited mesh using the right click" });
+    Elements.TRIGGERS.push({ name: "OnCenterPickTrigger", text: "center pick", properties: [], description: "When the user picks the edited mesh using the click of the mouse wheel" });
+    Elements.TRIGGERS.push({ name: "OnPointerOverTrigger", text: "pointer over", properties: [], description: "When the user's mouse is over the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnPointerOutTrigger", text: "pointer out", properties: [], description: "When the user's mouse is out of the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnEveryFrameTrigger", text: "every frame", properties: [], description: "This trigger is called each frame (only on scene)" });
+    Elements.TRIGGERS.push({ name: "OnIntersectionEnterTrigger", text: "intersection enter", properties: [{ targetType: "MeshProperties", text: "parameter", value: "Object name?" }], description: "When the edited mesh intersects the another mesh predefined in the options" });
+    Elements.TRIGGERS.push({ name: "OnIntersectionExitTrigger", text: "intersection exit", properties: [{ targetType: "MeshProperties", text: "parameter", value: "Object name?" }], description: "When the edited mesh exits intersection with the another mesh predefined in the options" });
+    Elements.TRIGGERS.push({ name: "OnKeyDownTrigger", text: "key down", properties: [{ targetType: null, text: "parameter:", value: "a" }], description: "When the user pressed a key (enter the key character, example: \"r\")" });
+    Elements.TRIGGERS.push({ name: "OnKeyUpTrigger", text: "key up", properties: [{ targetType: null, text: "parameter:", value: "a" }], description: "When the user unpressed a key (enter the key character, example: \"p\")" });
+    // Configure actions
+    Elements.ACTIONS.push({ name: "SwitchBooleanAction", text: "switch boolean", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }], description: "Switches the boolean value of a given parameter of the target object: true to false, or false to true" });
+    Elements.ACTIONS.push({ name: "SetStateAction", text: "set state", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "value", value: "" }], description: "Sets a new state value for the target object (example: \"off\" or \"on\")" });
+    Elements.ACTIONS.push({ name: "SetValueAction", text: "set value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }], description: "Sets a new value to the specified parameter of the target object (example: position.x to 0.0)" });
+    Elements.ACTIONS.push({ name: "SetParentAction", text: "set parent", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "parent", value: "" }], description: "Sets the new parent of the target object (example: a mesh or a light)" });
+    Elements.ACTIONS.push({ name: "IncrementValueAction", text: "increment value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }], description: "Increments the value of the given parameter of the target object. The value can be negative. (example: increment position.x of 5.0)" });
+    Elements.ACTIONS.push({ name: "PlayAnimationAction", text: "play animation", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "from", value: "0" }, { text: "to", value: "150" }, { text: "loop", value: "false" }], description: "Plays an animation of the target object. Specify the start frame, the end frame and if the animation should loop." });
+    Elements.ACTIONS.push({ name: "StopAnimationAction", text: "stop animation", properties: [{ targetType: "MeshProperties", text: "target", value: "" }], description: "Stops the animations of the target object." });
+    Elements.ACTIONS.push({ name: "DoNothingAction", text: "do nothing", properties: [], description: "Does nothing, can be used to balance/equilibrate the actions graph." });
+    Elements.ACTIONS.push({ name: "InterpolateValueAction", text: "interpolate value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "0" }, { text: "duration", value: "1000" }, { text: "stopOtherAnimations", value: "false" }], description: "Creates an animation (key frames) that animates the target object by interpolating the given parameter of the target value." });
+    Elements.ACTIONS.push({ name: "PlaySoundAction", text: "play sound", properties: [{ text: "sound", value: "" }], description: "Plays the specified sound." });
+    Elements.ACTIONS.push({ name: "StopSoundAction", text: "stop sound", properties: [{ text: "sound", value: "" }], description: "Stops the specified sound." });
+    Elements.ACTIONS.push({ name: "CombineAction", text: "combine", properties: [], description: "Special action that combines multiple actions. The combined actions are executed at the same time. Drag'n'drop the new actions inside to combine actions." });
+    // Configure flow control
+    Elements.FLOW_CONTROLS.push({ name: "ValueCondition", text: "value condition", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }, { text: "operator", value: SceneElements.OPERATORS[0] }], description: "A condition checking if a given value is equal, different, lesser or greater than the given parameter of the target object" });
+    Elements.FLOW_CONTROLS.push({ name: "StateCondition", text: "state condition", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "value", value: "" }], description: "A condition checking if the target object's state is equal to the given state. See \"set state\" action to set a state to an object." });
+    Elements.FLOW_CONTROLS.push({ name: "Hub", text: "hub", properties: [], description: "The hub is internally used by the Combine Action. It allows to add children to the Combine Action" });
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 251 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.list.js

@@ -0,0 +1,251 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var ListElement = (function () {
+        function ListElement() {
+            this.rect = null;
+            this.text = null;
+            this.name = "";
+            this.type = ActionsBuilder.Type.TRIGGER;
+            this.element = null;
+        }
+        return ListElement;
+    })();
+    ActionsBuilder.ListElement = ListElement;
+    var List = (function () {
+        /**
+        * Constructor
+        */
+        function List(viewer) {
+            var _this = this;
+            this._listElements = new Array();
+            // Get HTML elements
+            this.listElement = document.getElementById("ListsElementID");
+            this.triggersElement = document.getElementById("TriggersListID");
+            this.actionsElement = document.getElementById("ActionsListID");
+            this.flowControlsElement = document.getElementById("FlowActionsListID");
+            this._parentContainer = document.getElementById("ParentContainerID");
+            // Configure this
+            this._viewer = viewer;
+            // Create elements (lists)
+            this.triggersList = Raphael("TriggersListID", (25 * screen.width) / 100, 400);
+            this.actionsList = Raphael("ActionsListID", (25 * screen.width) / 100, 400);
+            this.flowControlsList = Raphael("FlowActionsListID", (25 * screen.width) / 100, 400);
+            // Manage events
+            window.addEventListener("resize", function (event) {
+                _this.onResize(event);
+            });
+        }
+        Object.defineProperty(List, "ELEMENT_HEIGHT", {
+            get: function () {
+                return 25;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+        * Resize event that resizes the list element dynamically
+        * @param event: the resize event
+        */
+        List.prototype.onResize = function (event) {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.listElement.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 + "px";
+            var listElementWidth = this.listElement.getBoundingClientRect().width;
+            for (var i = 0; i < this._listElements.length; i++) {
+                var rect = this._listElements[i].rect;
+                rect.attr("width", listElementWidth - 40);
+            }
+            this.triggersList.setSize(listElementWidth, this.triggersList.height);
+            this.actionsList.setSize(listElementWidth, this.triggersList.height);
+            this.flowControlsList.setSize(listElementWidth, this.triggersList.height);
+        };
+        List.prototype.createListsElements = function () {
+            var excludedTriggers = [6, 9, 10];
+            var yPosition = 10;
+            var textColor = Raphael.rgb(61, 72, 76);
+            var whiteColor = Raphael.rgb(255, 255, 255);
+            var configureTitle = function (listElement, rectColor) {
+                listElement.text.attr("x", 15);
+                listElement.rect.attr("fill", rectColor);
+                listElement.text.attr("font-family", "Sinkin Sans Medium");
+                listElement.text.attr("font-size", "11");
+            };
+            // Create triggers
+            var triggers = this._createListElement(this.triggersList, yPosition, "TRIGGERS", ActionsBuilder.Type.TRIGGER, whiteColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(triggers, Raphael.rgb(41, 129, 255));
+            for (var i = 0; i < ActionsBuilder.Elements.TRIGGERS.length; i++) {
+                var element = ActionsBuilder.Elements.TRIGGERS[i];
+                if (this._viewer.root.type === ActionsBuilder.Type.OBJECT && excludedTriggers.indexOf(i) !== -1) {
+                    continue;
+                }
+                var trigger = this._createListElement(this.triggersList, yPosition, element.text, ActionsBuilder.Type.TRIGGER, textColor, true, element);
+                trigger.rect.attr("fill", Raphael.rgb(133, 154, 185));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+            yPosition += List.ELEMENT_HEIGHT;
+            this.triggersElement.style.height = this.triggersList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.triggersList, this.triggersElement, triggers, yPosition);
+            // Create actions
+            yPosition = 10;
+            var actions = this._createListElement(this.actionsList, yPosition, "ACTIONS", ActionsBuilder.Type.ACTION, textColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(actions, Raphael.rgb(255, 220, 42));
+            for (var i = 0; i < ActionsBuilder.Elements.ACTIONS.length; i++) {
+                var element = ActionsBuilder.Elements.ACTIONS[i];
+                var action = this._createListElement(this.actionsList, yPosition, element.text, ActionsBuilder.Type.ACTION, textColor, true, element);
+                action.rect.attr("fill", Raphael.rgb(182, 185, 132));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+            yPosition += List.ELEMENT_HEIGHT;
+            this.actionsElement.style.height = this.actionsList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.actionsList, this.actionsElement, actions, yPosition);
+            // Create flow controls
+            yPosition = 10;
+            var flowControls = this._createListElement(this.flowControlsList, yPosition, "FLOW CONTROLS", ActionsBuilder.Type.FLOW_CONTROL, whiteColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(flowControls, Raphael.rgb(255, 41, 53));
+            for (var i = 0; i < ActionsBuilder.Elements.FLOW_CONTROLS.length - 1; i++) {
+                var element = ActionsBuilder.Elements.FLOW_CONTROLS[i];
+                var flowControl = this._createListElement(this.flowControlsList, yPosition, element.text, ActionsBuilder.Type.FLOW_CONTROL, textColor, true, element);
+                flowControl.rect.attr("fill", Raphael.rgb(185, 132, 140));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+            yPosition += List.ELEMENT_HEIGHT;
+            this.flowControlsElement.style.height = this.flowControlsList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.flowControlsList, this.flowControlsElement, flowControls, yPosition);
+        };
+        /**
+        * Clears the list of elements and removes the elements
+        */
+        List.prototype.clearLists = function () {
+            for (var i = 0; i < this._listElements.length; i++) {
+                this._removeListElement(this._listElements[i]);
+            }
+            this._listElements.splice(0, this._listElements.length - 1);
+        };
+        /**
+        * Sets the color theme of the lists
+        * @param color: the theme color
+        */
+        List.prototype.setColorTheme = function (color) {
+            this.triggersList.canvas.style.backgroundColor = color;
+            this.actionsList.canvas.style.backgroundColor = color;
+            this.flowControlsList.canvas.style.backgroundColor = color;
+        };
+        /**
+        * Creates a list element
+        * @param paper: the Raphael.js paper
+        * @param yPosition: the y position of the element
+        * @param text: the element text
+        * @param type: the element type (trigger, action, flow control)
+        * @param textColor: the text color
+        * @param drag: if the element should be drag'n'dropped
+        */
+        List.prototype._createListElement = function (paper, yPosition, text, type, textColor, drag, element) {
+            var object = new ListElement();
+            object.rect = paper.rect(10, yPosition, 300, List.ELEMENT_HEIGHT);
+            object.text = paper.text(30, yPosition + object.rect.attr("height") / 2, text);
+            object.text.attr("fill", textColor);
+            object.text.attr("text-anchor", "start");
+            object.text.attr("font-size", "12");
+            object.text.attr("text-anchor", "start");
+            object.text.attr("font-family", "Sinkin Sans Light");
+            if (drag) {
+                this._createListElementAnimation(object);
+            }
+            object.type = type;
+            object.element = element;
+            this._listElements.push(object);
+            return object;
+        };
+        /**
+        * Removes a list element
+        * @param element: the element to remove
+        */
+        List.prototype._removeListElement = function (element) {
+            element.rect.remove();
+            element.text.remove();
+        };
+        /*
+        * Creates the collapse animation of a list
+        * @param paper: the list paper
+        * @param htmlElement: the list div container
+        * @param element: the list element to click on
+        * @param expandedHeight: the height when the list is expanded
+        */
+        List.prototype._createCollapseAnimation = function (paper, htmlElement, element, expandedHeight) {
+            var onClick = function (event) {
+                var height = htmlElement.style.height;
+                if (height === expandedHeight + "px") {
+                    htmlElement.style.height = paper.canvas.style.height = 35 + "px";
+                }
+                else {
+                    htmlElement.style.height = paper.canvas.style.height = expandedHeight + "px";
+                }
+            };
+            element.rect.click(onClick);
+        };
+        /*
+        * Creates the animation of a list element
+        * @param element: the list element to animate
+        */
+        List.prototype._createListElementAnimation = function (element) {
+            var _this = this;
+            var onMove = function (dx, dy, x, y) { };
+            var onStart = function (x, y, event) {
+                _this._parentContainer.style.cursor = "copy";
+                element.rect.animate({
+                    x: -10,
+                    opacity: 0.25
+                }, 500, ">");
+                element.text.animate({
+                    x: 10,
+                    opacity: 0.25
+                }, 500, ">");
+            };
+            var onEnd = function (event) {
+                _this._parentContainer.style.cursor = "default";
+                element.rect.animate({
+                    x: 10,
+                    opacity: 1.0
+                }, 500, "<");
+                element.text.animate({
+                    x: 30,
+                    opacity: 1.0
+                }, 500, "<");
+                var dragResult = _this._viewer.traverseGraph(null, _this._viewer.mousex, _this._viewer.mousey, false);
+                if (dragResult.hit) {
+                    if (element.type === ActionsBuilder.Type.TRIGGER && dragResult.action !== _this._viewer.root) {
+                        alert("Triggers can be dragged only on the root node (the mesh)");
+                        return;
+                    }
+                    if (element.type === ActionsBuilder.Type.ACTION && dragResult.action === _this._viewer.root) {
+                        alert("Please add a trigger before.");
+                        return;
+                    }
+                    if (element.type === ActionsBuilder.Type.FLOW_CONTROL && (dragResult.action === _this._viewer.root || (dragResult.action.type === ActionsBuilder.Type.FLOW_CONTROL && dragResult.action.parent.hub === null))) {
+                        return;
+                    }
+                    if (element.type === ActionsBuilder.Type.FLOW_CONTROL && dragResult.action.combineArray !== null) {
+                        alert("A condition cannot be handled by a Combine Action.");
+                        return;
+                    }
+                    if ((element.type === ActionsBuilder.Type.FLOW_CONTROL || element.type === ActionsBuilder.Type.ACTION) && dragResult.action.type === ActionsBuilder.Type.TRIGGER && dragResult.action.children.length > 0) {
+                        alert("Triggers can have only one child. Please add another trigger of same type.");
+                        return;
+                    }
+                    if (!(dragResult.action.combineArray !== null) && dragResult.action.children.length > 0 && dragResult.action.type !== ActionsBuilder.Type.TRIGGER && dragResult.action !== _this._viewer.root) {
+                        alert("An action can have only one child.");
+                        return;
+                    }
+                    _this._viewer.addAction(dragResult.action, element.type, element.element);
+                    _this._viewer.update();
+                }
+            };
+            element.rect.drag(onMove, onStart, onEnd);
+            element.text.drag(onMove, onStart, onEnd);
+        };
+        return List;
+    })();
+    ActionsBuilder.List = List;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 98 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.main.js

@@ -0,0 +1,98 @@
+/*
+Global functions called by the plugins (3ds Max, etc.)
+*/
+// Elements
+var list = null;
+var viewer = null;
+var actionsBuilderJsonInput = document.getElementById("ActionsBuilderJSON");
+this.createJSON = function () {
+    var structure = viewer.utils.createJSON(viewer.root);
+    var asText = JSON.stringify(structure);
+    actionsBuilderJsonInput.value = asText;
+};
+this.loadFromJSON = function () {
+    var json = actionsBuilderJsonInput.value;
+    if (json !== "") {
+        var structure = JSON.parse(json);
+        viewer.utils.loadFromJSON(structure, null);
+    }
+};
+this.updateObjectName = function () {
+    var element = document.getElementById("ActionsBuilderObjectName");
+    var name = element.value;
+    viewer.objectName = name;
+    if (viewer.root.type === ActionsBuilder.Type.OBJECT) {
+        name += " - Mesh";
+    }
+    else {
+        name += " - Scene";
+    }
+    viewer.root.node.text.attr("text", name);
+};
+this.resetList = function () {
+    list.clearLists();
+    list.createListsElements();
+};
+this.setMeshesNames = function () {
+    var args = [];
+    for (var _i = 0; _i < arguments.length; _i++) {
+        args[_i - 0] = arguments[_i];
+    }
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.MESHES.push(args[i]);
+    }
+};
+this.setLightsNames = function () {
+    var args = [];
+    for (var _i = 0; _i < arguments.length; _i++) {
+        args[_i - 0] = arguments[_i];
+    }
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.LIGHTS.push(args[i]);
+    }
+};
+this.setCamerasNames = function () {
+    var args = [];
+    for (var _i = 0; _i < arguments.length; _i++) {
+        args[_i - 0] = arguments[_i];
+    }
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.CAMERAS.push(args[i]);
+    }
+};
+this.setSoundsNames = function () {
+    var args = [];
+    for (var _i = 0; _i < arguments.length; _i++) {
+        args[_i - 0] = arguments[_i];
+    }
+    for (var i = 0; i < args.length; i++) {
+        var sound = args[i];
+        if (sound !== "" && ActionsBuilder.SceneElements.SOUNDS.indexOf(sound) === -1) {
+            ActionsBuilder.SceneElements.SOUNDS.push(args[i]);
+        }
+    }
+};
+this.hideButtons = function () {
+    // Empty
+};
+this.setIsObject = function () {
+    viewer.root.type = ActionsBuilder.Type.OBJECT;
+};
+this.setIsScene = function () {
+    viewer.root.type = ActionsBuilder.Type.SCENE;
+};
+this.run = function () {
+    // Configure viewer
+    viewer = new ActionsBuilder.Viewer(ActionsBuilder.Type.OBJECT);
+    viewer.setColorTheme("-ms-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("-webkit-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("-o-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.onResize();
+    viewer.update();
+    // Configure list
+    list = new ActionsBuilder.List(viewer);
+    list.setColorTheme("rgb(64, 64, 64)");
+    list.createListsElements();
+    list.onResize();
+};

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2551 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.max.js


+ 483 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.parameters.js

@@ -0,0 +1,483 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var Parameters = (function () {
+        /*
+        * Constructor
+        */
+        function Parameters(viewer) {
+            var _this = this;
+            this._action = null;
+            // Get HTML elements
+            this.parametersContainer = document.getElementById("ParametersElementID");
+            this.parametersHelpElement = document.getElementById("ParametersHelpElementID");
+            // Configure this
+            this._viewer = viewer;
+            // Configure events
+            window.addEventListener("resize", function (event) {
+                _this.onResize(event);
+            });
+        }
+        /*
+        * Clears the parameters fileds in the parameters view
+        */
+        Parameters.prototype.clearParameters = function () {
+            if (this.parametersContainer.children === null) {
+                return;
+            }
+            while (this.parametersContainer.children.length > 0) {
+                this.parametersContainer.removeChild(this.parametersContainer.firstChild);
+            }
+        };
+        /*
+        * Creates parameters fields
+        * @param action: the action to configure
+        */
+        Parameters.prototype.createParameters = function (action) {
+            // Clear parameters fields and draw help description
+            this._action = action;
+            this.clearParameters();
+            if (action === null) {
+                return;
+            }
+            this._createHelpSection(action);
+            this._createNodeSection(action);
+            // Get properties
+            var properties = action.properties;
+            var propertiesResults = action.propertiesResults;
+            var targetParameterSelect = null;
+            var targetParameterNameSelect = null;
+            var propertyPathSelect = null;
+            var propertyPathOptionalSelect = null;
+            if (properties.length === 0) {
+                return;
+            }
+            // Draw properties
+            for (var i = 0; i < properties.length; i++) {
+                // Create separator
+                var separator = document.createElement("hr");
+                separator.noShade = true;
+                separator.className = "ParametersElementSeparatorClass";
+                this.parametersContainer.appendChild(separator);
+                // Create parameter text
+                var parameterName = document.createElement("a");
+                parameterName.text = properties[i].text;
+                parameterName.className = "ParametersElementTitleClass";
+                this.parametersContainer.appendChild(parameterName);
+                if (properties[i].text === "parameter" || properties[i].text === "target" || properties[i].text === "parent") {
+                    // Create target select element
+                    targetParameterSelect = document.createElement("select");
+                    targetParameterSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(targetParameterSelect);
+                    // Create target name select element
+                    targetParameterNameSelect = document.createElement("select");
+                    targetParameterNameSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(targetParameterNameSelect);
+                    // Events and configure
+                    (this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i))(null);
+                    targetParameterSelect.value = propertiesResults[i].targetType;
+                    targetParameterNameSelect.value = propertiesResults[i].value;
+                    targetParameterSelect.onchange = this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i);
+                    targetParameterNameSelect.onchange = this._parameterTargetNameChanged(targetParameterSelect, targetParameterNameSelect, i);
+                }
+                else if (properties[i].text === "propertyPath") {
+                    // Create property path select
+                    propertyPathSelect = document.createElement("select");
+                    propertyPathSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(propertyPathSelect);
+                    // Create additional select
+                    propertyPathOptionalSelect = document.createElement("select");
+                    propertyPathOptionalSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(propertyPathOptionalSelect);
+                    // Events and configure
+                    (this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect, i))(null);
+                    var property = this._action.propertiesResults[i].value.split(".");
+                    if (property.length > 0) {
+                        if (property.length === 1) {
+                            propertyPathSelect.value = property[0];
+                        }
+                        else {
+                            var completePropertyPath = "";
+                            for (var j = 0; j < property.length - 1; j++) {
+                                completePropertyPath += property[j];
+                                completePropertyPath += (j === property.length - 2) ? "" : ".";
+                            }
+                            propertyPathSelect.value = completePropertyPath;
+                            this._viewer.utils.setElementVisible(propertyPathOptionalSelect, true);
+                        }
+                        this._fillAdditionalPropertyPath(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect);
+                        propertyPathOptionalSelect.value = property[property.length - 1];
+                        if (propertyPathOptionalSelect.options.length === 0 || propertyPathOptionalSelect.options[0].text === "") {
+                            this._viewer.utils.setElementVisible(propertyPathOptionalSelect, false);
+                        }
+                    }
+                    targetParameterSelect.onchange = this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i - 1);
+                    propertyPathSelect.onchange = this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect, i);
+                    propertyPathOptionalSelect.onchange = this._additionalPropertyPathSelectChanged(propertyPathSelect, propertyPathOptionalSelect, i);
+                }
+                else if (properties[i].text === "operator") {
+                    var conditionOperatorSelect = document.createElement("select");
+                    conditionOperatorSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(conditionOperatorSelect);
+                    // Configure event
+                    (this._conditionOperatorSelectChanged(conditionOperatorSelect, i))(null);
+                    conditionOperatorSelect.value = propertiesResults[i].value;
+                    conditionOperatorSelect.onchange = this._conditionOperatorSelectChanged(conditionOperatorSelect, i);
+                }
+                else if (properties[i].text === "sound") {
+                    var soundSelect = document.createElement("select");
+                    soundSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(soundSelect);
+                    // Configure event
+                    (this._soundSelectChanged(soundSelect, i))(null);
+                    soundSelect.value = propertiesResults[i].value;
+                    soundSelect.onchange = this._soundSelectChanged(soundSelect, i);
+                }
+                else {
+                    if (propertiesResults[i].value === "true" || propertiesResults[i].value === "false") {
+                        var booleanSelect = document.createElement("select");
+                        booleanSelect.className = "ParametersElementSelectClass";
+                        this.parametersContainer.appendChild(booleanSelect);
+                        // Configure event
+                        (this._booleanSelectChanged(booleanSelect, i))(null);
+                        booleanSelect.value = propertiesResults[i].value;
+                        booleanSelect.onchange = this._booleanSelectChanged(booleanSelect, i);
+                    }
+                    else {
+                        var propertyInput = document.createElement("input");
+                        propertyInput.value = propertiesResults[i].value;
+                        propertyInput.className = "ParametersElementInputClass";
+                        this.parametersContainer.appendChild(propertyInput);
+                        // Configure event
+                        propertyInput.onkeyup = this._propertyInputChanged(propertyInput, i);
+                    }
+                }
+            }
+        };
+        /*
+        * Resizes the parameters view
+        * @param: the resize event
+        */
+        Parameters.prototype.onResize = function (event) {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.parametersContainer.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 200 + "px";
+            this.parametersHelpElement.style.height = 200 + "px";
+        };
+        /*
+        * Returns the boolean select change event
+        * @param booleanSelect: the boolean select element
+        * @param indice: the properties result indice
+        */
+        Parameters.prototype._booleanSelectChanged = function (booleanSelect, indice) {
+            var _this = this;
+            return function (ev) {
+                if (booleanSelect.options.length === 0) {
+                    var values = ["true", "false"];
+                    for (var i = 0; i < values.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = values[i];
+                        booleanSelect.options.add(option);
+                    }
+                }
+                else {
+                    _this._action.propertiesResults[indice].value = booleanSelect.value;
+                }
+            };
+        };
+        /*
+        * Returns the sound select change event
+        * @param soundSelect: the sound select element
+        * @param indice: the properties result indice
+        */
+        Parameters.prototype._soundSelectChanged = function (soundSelect, indice) {
+            var _this = this;
+            return function (ev) {
+                if (soundSelect.options.length === 0) {
+                    for (var i = 0; i < ActionsBuilder.SceneElements.SOUNDS.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = ActionsBuilder.SceneElements.SOUNDS[i];
+                        soundSelect.options.add(option);
+                    }
+                    _this._sortList(soundSelect);
+                }
+                else {
+                    _this._action.propertiesResults[indice].value = soundSelect.value;
+                }
+            };
+        };
+        /*
+        * Returns the condition opeator select changed event
+        * @param conditionOperatorSelect: the condition operator select element
+        * @param indice: the properties result indice
+        */
+        Parameters.prototype._conditionOperatorSelectChanged = function (conditionOperatorSelect, indice) {
+            var _this = this;
+            return function (ev) {
+                if (conditionOperatorSelect.options.length === 0) {
+                    for (var i = 0; i < ActionsBuilder.SceneElements.OPERATORS.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = ActionsBuilder.SceneElements.OPERATORS[i];
+                        conditionOperatorSelect.options.add(option);
+                    }
+                }
+                else {
+                    _this._action.propertiesResults[indice].value = conditionOperatorSelect.value;
+                }
+            };
+        };
+        /*
+        * Returns the property input changed event
+        * @param propertyInput: the property input
+        * @param indice: the properties result indice
+        */
+        Parameters.prototype._propertyInputChanged = function (propertyInput, indice) {
+            var _this = this;
+            return function (ev) {
+                _this._action.propertiesResults[indice].value = propertyInput.value;
+            };
+        };
+        /*
+        * Returns the propertyPath select changed event
+        * @param targetParameterSelect: the target/parameter select element
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        Parameters.prototype._propertyPathSelectChanged = function (targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect, indice) {
+            var _this = this;
+            return function (event) {
+                if (propertyPathSelect.options.length === 0) {
+                    // Configure start values
+                    var properties = _this._getPropertiesFromType(targetParameterSelect.value);
+                    if (properties !== null) {
+                        for (var i = 0; i < properties.length; i++) {
+                            var option = document.createElement("option");
+                            option.value = option.text = properties[i];
+                            propertyPathSelect.options.add(option);
+                        }
+                    }
+                }
+                else {
+                    // Set property
+                    _this._action.propertiesResults[indice].value = propertyPathSelect.value;
+                }
+                // Configure addition property
+                _this._fillAdditionalPropertyPath(targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect);
+                // Sort
+                _this._sortList(propertyPathSelect);
+            };
+        };
+        Parameters.prototype._fillAdditionalPropertyPath = function (targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect) {
+            additionalPropertyPathSelect.options.length = 0;
+            var object = this._getObjectFromType(targetParameterSelect.value);
+            if (object !== null) {
+                var propertyPath = propertyPathSelect.value.split(".");
+                for (var i = 0; i < propertyPath.length; i++) {
+                    object = object[propertyPath[i]];
+                }
+            }
+            if (object === null || object === undefined || (typeof (object)).toLowerCase() === "string") {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, false);
+                return;
+            }
+            // Add options
+            var emptyOption = document.createElement("option");
+            emptyOption.value = emptyOption.text = "";
+            additionalPropertyPathSelect.add(emptyOption);
+            for (var thing in object) {
+                var type = ActionsBuilder.SceneElements.GetInstanceOf(object[thing]);
+                var index = ActionsBuilder.SceneElements.TYPES.indexOf(type);
+                if (index !== -1) {
+                    var option = document.createElement("option");
+                    option.value = option.text = thing;
+                    additionalPropertyPathSelect.options.add(option);
+                    emptyOption.text += thing + ", ";
+                }
+            }
+            if (additionalPropertyPathSelect.options.length === 0 || additionalPropertyPathSelect.options[0].text === "") {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, false);
+            }
+            else {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, true);
+            }
+        };
+        /*
+        * Returns the additional propertyPath select changed event
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        Parameters.prototype._additionalPropertyPathSelectChanged = function (propertyPathSelect, additionalPropertyPathSelect, indice) {
+            var _this = this;
+            return function (event) {
+                var property = propertyPathSelect.value;
+                var additionalProperty = additionalPropertyPathSelect.value;
+                if (additionalProperty !== "") {
+                    property += ".";
+                    property += additionalPropertyPathSelect.value;
+                }
+                _this._action.propertiesResults[indice].value = property;
+            };
+        };
+        /*
+        * Returns the parameter/target select changed event
+        * @param targetParameterSelect: the target/parameter select element
+        * @param targetParameterNameSelect: the target/parameter name select element
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        Parameters.prototype._parameterTargetChanged = function (targetParameterSelect, targetParameterNameSelect, propertyPathSelect, additionalPropertyPathSelect, indice) {
+            var _this = this;
+            return function (event) {
+                if (targetParameterSelect.options.length === 0) {
+                    // Configure start values
+                    var options = [
+                        { text: "Mesh", targetType: "MeshProperties" },
+                        { text: "Light", targetType: "LightProperties" },
+                        { text: "Camera", targetType: "CameraProperties" },
+                        { text: "Scene", targetType: "SceneProperties" }
+                    ];
+                    targetParameterSelect.options.length = 0;
+                    for (var i = 0; i < options.length; i++) {
+                        var option = document.createElement("option");
+                        option.text = options[i].text;
+                        option.value = options[i].targetType;
+                        targetParameterSelect.options.add(option);
+                    }
+                }
+                else {
+                    _this._action.propertiesResults[indice].targetType = targetParameterSelect.value;
+                    _this._action.propertiesResults[indice].value = "";
+                    if (propertyPathSelect !== null) {
+                        _this._action.propertiesResults[indice + 1].value = ""; // propertyPath
+                    }
+                }
+                // Configure target names
+                var targetParameterProperties = _this._getTargetFromType(targetParameterSelect.value);
+                targetParameterNameSelect.options.length = 0;
+                if (targetParameterProperties !== null) {
+                    for (var i = 0; i < targetParameterProperties.length; i++) {
+                        var option = document.createElement("option");
+                        option.text = option.value = targetParameterProperties[i];
+                        targetParameterNameSelect.options.add(option);
+                    }
+                }
+                // Clear property path
+                if (propertyPathSelect !== null) {
+                    propertyPathSelect.options.length = 0;
+                    additionalPropertyPathSelect.options.length = 0;
+                    _this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect, indice + 1)(null);
+                }
+                _this._sortList(targetParameterNameSelect);
+                _this._sortList(targetParameterSelect);
+            };
+        };
+        /*
+        * Returns the parameter/target name select changed
+        * @param indice: the properties indice to change
+        */
+        Parameters.prototype._parameterTargetNameChanged = function (targetParameterSelect, targetParameterNameSelect, indice) {
+            var _this = this;
+            return function (event) {
+                _this._action.propertiesResults[indice].value = targetParameterNameSelect.value;
+            };
+        };
+        /*
+        * Returns the array of objects names in function of its type
+        * @param type: the target type
+        */
+        Parameters.prototype._getTargetFromType = function (type) {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return ActionsBuilder.SceneElements.MESHES;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return ActionsBuilder.SceneElements.LIGHTS;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return ActionsBuilder.SceneElements.CAMERAS;
+            }
+            return null;
+        };
+        /*
+        * Returns the properties in function of its type
+        * @param type: the target type
+        */
+        Parameters.prototype._getPropertiesFromType = function (type) {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return ActionsBuilder.SceneElements.MESH_PROPERTIES;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return ActionsBuilder.SceneElements.LIGHT_PROPERTIES;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return ActionsBuilder.SceneElements.CAMERA_PROPERTIES;
+            }
+            if (type === "SceneProperties" || type === "Scene") {
+                return ActionsBuilder.SceneElements.SCENE_PROPERTIES;
+            }
+            return null;
+        };
+        /*
+        * Returns the object in function of the given type
+        * @param type: the target type
+        */
+        Parameters.prototype._getObjectFromType = function (type) {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return ActionsBuilder.SceneElements.MESH;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return ActionsBuilder.SceneElements.LIGHT;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return ActionsBuilder.SceneElements.CAMERA;
+            }
+            if (type === "SceneProperties" || type === "Scene") {
+                return ActionsBuilder.SceneElements.SCENE;
+            }
+            return null;
+        };
+        /*
+        * Creates the node section (top of parameters)
+        * @param action: the action element to get color, text, name etc.
+        */
+        Parameters.prototype._createNodeSection = function (action) {
+            var element = document.createElement("div");
+            element.style.background = this._viewer.getSelectedNodeColor(action.type, action.node.detached);
+            element.className = "ParametersElementNodeClass";
+            var text = document.createElement("a");
+            text.text = action.name;
+            text.className = "ParametersElementNodeTextClass";
+            element.appendChild(text);
+            this.parametersContainer.appendChild(element);
+        };
+        /*
+        * Creates the help section
+        * @param action : the action containing the description
+        */
+        Parameters.prototype._createHelpSection = function (action) {
+            // Get description
+            var element = ActionsBuilder.Elements.GetElementFromName(action.name);
+            if (element !== null) {
+                this.parametersHelpElement.textContent = element.description;
+            }
+        };
+        /*
+        * Alphabetically sorts a HTML select element options
+        * @param element : the HTML select element to sort
+        */
+        Parameters.prototype._sortList = function (element) {
+            var options = [];
+            for (var i = element.options.length - 1; i >= 0; i--) {
+                options.push(element.removeChild(element.options[i]));
+            }
+            options.sort(function (a, b) {
+                return a.innerHTML.localeCompare(b.innerHTML);
+            });
+            for (var i = 0; i < options.length; i++) {
+                element.options.add(options[i]);
+            }
+        };
+        return Parameters;
+    })();
+    ActionsBuilder.Parameters = Parameters;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 85 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.toolbar.js

@@ -0,0 +1,85 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var Toolbar = (function () {
+        function Toolbar(viewer) {
+            var _this = this;
+            // Get HTML elements
+            this.toolbarElement = document.getElementById("ToolbarElementID");
+            // Configure this
+            this._viewer = viewer;
+            // Manage events
+            window.addEventListener("resize", function (event) {
+                _this.onResize();
+            });
+            // Bottom toolbar
+            document.getElementById("ViewerDeZoomID").addEventListener("click", function (event) {
+                if (_this._viewer.zoom > 0.1) {
+                    _this._viewer.zoom -= 0.1;
+                }
+                _this._viewer.update();
+            });
+            document.getElementById("ViewerZoomID").addEventListener("click", function (event) {
+                if (_this._viewer.zoom < 1.0) {
+                    _this._viewer.zoom += 0.1;
+                }
+                _this._viewer.update();
+            });
+            document.getElementById("ViewerReconnectAll").addEventListener("click", function (event) {
+                for (var i = 0; i < _this._viewer.root.children.length; i++) {
+                    _this._viewer.selectedNode = _this._viewer.root.children[i];
+                    _this._viewer.utils.onDetachAction(false, true);
+                }
+                _this._viewer.update();
+                _this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerDisconnectAll").addEventListener("click", function (event) {
+                for (var i = 0; i < _this._viewer.root.children.length; i++) {
+                    _this._viewer.selectedNode = _this._viewer.root.children[i];
+                    _this._viewer.utils.onDetachAction(true, false);
+                }
+                _this._viewer.update();
+                _this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerReduceAll").addEventListener("click", function (event) {
+                for (var i = 0; i < _this._viewer.root.children.length; i++) {
+                    _this._viewer.selectedNode = _this._viewer.root.children[i];
+                    _this._viewer.utils.onReduceAll(false);
+                }
+                _this._viewer.update();
+                _this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerExpandAll").addEventListener("click", function (event) {
+                for (var i = 0; i < _this._viewer.root.children.length; i++) {
+                    _this._viewer.selectedNode = _this._viewer.root.children[i];
+                    _this._viewer.utils.onReduceAll(true);
+                }
+                _this._viewer.update();
+                _this._viewer.selectedNode = null;
+            });
+            // Top toolbar
+            this.saveActionGraphElement = document.getElementById("ToolsButtonIDSaveActionGraph");
+            this.drawSaveActionGraphButton(false);
+            document.getElementById("ResetActionGraphID").addEventListener("click", function (event) {
+                if (confirm("Are you sure?")) {
+                    for (var i = 0; i < _this._viewer.root.children.length; i++) {
+                        _this._viewer.selectedNode = _this._viewer.root.children[i];
+                        _this._viewer.utils.onRemoveBranch();
+                    }
+                    _this._viewer.update();
+                    _this._viewer.selectedNode = null;
+                }
+            });
+            document.getElementById("TestActionGraphID").addEventListener("click", function (event) {
+                _this._viewer.utils.onTestGraph();
+            });
+        }
+        Toolbar.prototype.onResize = function () {
+            this.toolbarElement.style.top = this._viewer.viewerElement.clientHeight + 20 + "px";
+        };
+        Toolbar.prototype.drawSaveActionGraphButton = function (draw) {
+            this.saveActionGraphElement.style.display = draw ? "block" : "none";
+        };
+        return Toolbar;
+    })();
+    ActionsBuilder.Toolbar = Toolbar;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 433 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.utils.js

@@ -0,0 +1,433 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var Utils = (function () {
+        /*
+        * Constructor
+        * @param viewer: the viewer instance
+        */
+        function Utils(viewer) {
+            // Members
+            this.copiedStructure = null;
+            // Configure this
+            this._viewer = viewer;
+        }
+        /*
+        * Tests the graph and reports errors
+        */
+        Utils.prototype.onTestGraph = function () {
+            var _this = this;
+            if (this._viewer.root.children.length === 0) {
+                alert("Please add at least a Trigger and an Action to test the graph");
+            }
+            var onTestTarget = function (targetType, target) {
+                var targetExists = false;
+                var array = _this._viewer.parameters._getTargetFromType(targetType);
+                if (array === null) {
+                    return targetExists;
+                }
+                for (var i = 0; i < array.length; i++) {
+                    if (array[i] === target) {
+                        targetExists = true;
+                        break;
+                    }
+                }
+                return targetExists;
+            };
+            var onNodeError = function (action) {
+                var node = action.node;
+                node.rect.attr("fill", Raphael.rgb(255, 0, 0));
+                return false;
+            };
+            var onTestAction = function (action) {
+                console.log("Testing " + action.name);
+                if (action.combineArray !== null) {
+                    var foundError = false;
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        if (!onTestAction(action.combineArray[i])) {
+                            foundError = true;
+                        }
+                    }
+                    if (foundError) {
+                        return false;
+                    }
+                }
+                else {
+                    // Test properties
+                    var properties = action.properties;
+                    var propertiesResults = action.propertiesResults;
+                    if (properties !== null) {
+                        var object = null;
+                        var propertyPath = null;
+                        for (var i = 0; i < properties.length; i++) {
+                            // Target
+                            if (properties[i].text === "target" || properties[i].text === "parent") {
+                                object = _this._viewer.parameters._getObjectFromType(properties[i].targetType);
+                                var targetExists = onTestTarget(propertiesResults[i].targetType, propertiesResults[i].value);
+                                if (!targetExists) {
+                                    return onNodeError(action);
+                                }
+                            }
+                            else if (properties[i].text === "propertyPath") {
+                                var property = propertiesResults[i].value;
+                                var effectiveProperty = object;
+                                var p = property.split(".");
+                                for (var j = 0; j < p.length && effectiveProperty !== undefined; j++) {
+                                    effectiveProperty = effectiveProperty[p[j]];
+                                }
+                                if (effectiveProperty === undefined) {
+                                    return onNodeError(action);
+                                }
+                                else {
+                                    propertyPath = effectiveProperty;
+                                }
+                            }
+                            else if (properties[i].text == "value" && propertyPath != null) {
+                                var value = propertiesResults[i].value;
+                                if (!isNaN(propertyPath)) {
+                                    var num = parseFloat(value);
+                                    if (isNaN(num) || value === "") {
+                                        return onNodeError(action);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    var foundError = false;
+                    for (var i = 0; i < action.children.length; i++) {
+                        if (!onTestAction(action.children[i])) {
+                            foundError = true;
+                        }
+                    }
+                    return !foundError;
+                }
+            };
+            var root = this._viewer.root;
+            var foundError = false;
+            for (var i = 0; i < root.children.length; i++) {
+                var trigger = root.children[i];
+                var properties = trigger.properties;
+                // Test properties of trigger (parameter)
+                if (properties !== null && properties.length > 0) {
+                    // Only one property
+                    var parameter = trigger.propertiesResults[0].value;
+                    if (properties[0].targetType !== null) {
+                        // Intersection trigger
+                        if (!onTestTarget("MeshProperties", parameter)) {
+                            foundError = onNodeError(trigger);
+                        }
+                    }
+                    else {
+                        // Key trigger
+                        if (!parameter.match(/[a-z]/)) {
+                            foundError = onNodeError(trigger);
+                        }
+                    }
+                }
+                for (var j = 0; j < trigger.children.length; j++) {
+                    var child = trigger.children[j];
+                    var result = onTestAction(child);
+                    if (!result) {
+                        foundError = true;
+                    }
+                }
+            }
+            if (foundError) {
+                alert("Found error(s). the red nodes contain the error.");
+            }
+            else {
+                alert("No error found.");
+            }
+        };
+        /*
+        * Recursively reduce/expand nodes
+        */
+        Utils.prototype.onReduceAll = function (forceExpand) {
+            if (forceExpand === void 0) { forceExpand = false; }
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var action = this._viewer.selectedNode;
+            if (action.combineArray !== null) {
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    this._viewer.selectedNode = action.combineArray[i];
+                    this.onReduce(forceExpand, !forceExpand);
+                }
+            }
+            else {
+                this.onReduce(forceExpand, !forceExpand);
+            }
+            for (var i = 0; i < action.children.length; i++) {
+                this._viewer.selectedNode = action.children[i];
+                this.onReduceAll(forceExpand);
+            }
+        };
+        /*
+        * Reduces the selected node
+        */
+        Utils.prototype.onReduce = function (forceExpand, forceReduce) {
+            if (forceExpand === void 0) { forceExpand = false; }
+            if (forceReduce === void 0) { forceReduce = false; }
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var node = this._viewer.selectedNode.node;
+            node.rect.stop(node.rect.animation);
+            // Set minimized
+            if (forceExpand === true) {
+                node.minimized = false;
+            }
+            else if (forceReduce === true) {
+                node.minimized = true;
+            }
+            else {
+                node.minimized = !node.minimized;
+            }
+            // Set size
+            if (node.minimized) {
+                node.text.hide();
+                node.rect.attr("width", ActionsBuilder.Viewer.NODE_MINIMIZED_WIDTH * this._viewer.zoom);
+            }
+            else {
+                node.text.show();
+                node.rect.attr("width", ActionsBuilder.Viewer.NODE_WIDTH * this._viewer.zoom);
+            }
+        };
+        /*
+        * Detaches the selected action
+        */
+        Utils.prototype.onDetachAction = function (forceDetach, forceAttach) {
+            var _this = this;
+            if (forceDetach === void 0) { forceDetach = false; }
+            if (forceAttach === void 0) { forceAttach = false; }
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var action = this._viewer.selectedNode;
+            if (forceDetach === true) {
+                action.node.detached = true;
+            }
+            else if (forceAttach === true) {
+                action.node.detached = false;
+            }
+            else {
+                action.node.detached = !action.node.detached;
+            }
+            var onSetColor = function (root, detached) {
+                var rootNode = root.node;
+                rootNode.rect.attr("fill", _this._viewer.getNodeColor(root.type, detached));
+                if (root.combineArray !== null) {
+                    for (var i = 0; i < root.combineArray.length; i++) {
+                        var combineNode = root.combineArray[i].node;
+                        combineNode.rect.attr("fill", _this._viewer.getNodeColor(root.combineArray[i].type, detached));
+                    }
+                }
+                for (var i = 0; i < root.children.length; i++) {
+                    onSetColor(root.children[i], detached);
+                }
+            };
+            onSetColor(action, action.node.detached);
+        };
+        /*
+        * Removes the selected node
+        */
+        Utils.prototype.onRemoveNode = function () {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var action = this._viewer.selectedNode;
+            var parent = action.parent;
+            // If trigger, remove branch
+            if (action.type === ActionsBuilder.Type.TRIGGER) {
+                this.onRemoveBranch();
+                return;
+            }
+            // If it is a combine hub
+            if (action.type === ActionsBuilder.Type.FLOW_CONTROL && parent !== null && parent.combineArray !== null) {
+                action = parent;
+                parent = action.parent;
+            }
+            // Remove
+            if (parent !== null && parent.combineArray !== null) {
+                parent.removeCombinedAction(action);
+                if (parent.combineArray.length === 0) {
+                    parent.node.text.attr("text", "combine");
+                }
+            }
+            else {
+                if (action.combineArray !== null) {
+                    action.removeChild(action.hub);
+                }
+                action.parent.removeChild(action);
+            }
+            if (action.combineArray !== null) {
+                this._viewer.removeAction(action.hub, false);
+            }
+            this._viewer.removeAction(action, false);
+            // Finish
+            this._viewer.update();
+            this._viewer.parameters.clearParameters();
+            this._viewer.selectedNode = null;
+        };
+        /*
+        * Removes a branch starting from the selected node
+        */
+        Utils.prototype.onRemoveBranch = function () {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            if (this._viewer.selectedNode === this._viewer.root) {
+                alert("Cannot remove the root node");
+                return;
+            }
+            var action = this._viewer.selectedNode;
+            var parent = action.parent;
+            // If combine
+            if (action.parent !== null && action.parent.combineArray !== null) {
+                action = parent;
+                parent = action.parent;
+            }
+            // Remove
+            if (action.combineArray !== null) {
+                action.removeChild(action.hub);
+            }
+            action.parent.removeChild(action);
+            this._viewer.removeAction(action, true);
+            // Finish
+            this._viewer.update();
+            this._viewer.parameters.clearParameters();
+            this._viewer.selectedNode = null;
+        };
+        /*
+        * Copies the selected structure
+        */
+        Utils.prototype.onCopyStructure = function () {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var structure = this.createJSON(this._viewer.selectedNode);
+            var asText = JSON.stringify(structure);
+            if (window.clipboardData !== undefined) {
+                window.clipboardData.setData("text", asText);
+            }
+            else {
+                this.copiedStructure = asText;
+            }
+        };
+        /*
+        * Pastes the graph structure previously copied
+        */
+        Utils.prototype.onPasteStructure = function () {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+            var asText = (window.clipboardData !== undefined) ? window.clipboardData.getData("text") : this.copiedStructure;
+            var isJson = asText.length > 0 && asText[0] == "{" && asText[asText.length - 1] == "}";
+            var structure = JSON.parse(asText);
+            var action = this._viewer.selectedNode;
+            if (structure.type === ActionsBuilder.Type.TRIGGER && action !== this._viewer.root) {
+                alert("You can't paste a trigger if the selected node isn't the root object");
+                return;
+            }
+            if (structure.type !== ActionsBuilder.Type.TRIGGER && action === this._viewer.root) {
+                alert("You can't paste an action or condition if the selected node is the root object");
+                return;
+            }
+            this.loadFromJSON(structure, action);
+            this._viewer.update();
+        };
+        /*
+        * Loads a graph from JSON
+        * @pram graph: the graph structure
+        * @param startAction: the action to start load
+        */
+        Utils.prototype.loadFromJSON = function (graph, startAction) {
+            var _this = this;
+            // If startNode is null, means it replaces all the graph
+            // If not, it comes from a copy/paste
+            if (startAction === null) {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.removeAction(this._viewer.root.children[i], true);
+                }
+                this._viewer.root.clearChildren();
+            }
+            var load = function (root, parent, detached, combine) {
+                if (parent === null) {
+                    parent = _this._viewer.root;
+                }
+                var newAction = null;
+                if (root.type !== ActionsBuilder.Type.OBJECT && root.type !== ActionsBuilder.Type.SCENE) {
+                    var action = _this._viewer.addAction(parent, root.type, ActionsBuilder.Elements.GetElementFromName(root.name));
+                    for (var i = 0; i < root.properties.length; i++) {
+                        var targetType = root.properties[i].targetType;
+                        if (targetType === undefined) {
+                            targetType = "MeshProperties"; // Default is mesh properties
+                        }
+                        action.propertiesResults[i] = { value: root.properties[i].value, targetType: targetType };
+                    }
+                    var node = action.node;
+                    node.detached = root.detached;
+                    if (detached) {
+                        node.rect.attr("fill", _this._viewer.getNodeColor(action.type, detached));
+                    }
+                    // If combine array
+                    if (root.combine !== undefined) {
+                        for (var i = 0; i < root.combine.length; i++) {
+                            load(root.combine[i], action, detached, true);
+                        }
+                    }
+                    if (!combine) {
+                        parent = parent.children[parent.children.length - 1];
+                    }
+                }
+                for (var i = 0; i < root.children.length; i++) {
+                    load(root.children[i], newAction !== null && newAction.combineArray !== null ? newAction.hub : parent, root.detached, false);
+                }
+            };
+            // Finish
+            load(graph, startAction, false, false);
+            this._viewer.update();
+        };
+        /*
+        * Creates a JSON object starting from a root action
+        * @param root: the root action
+        */
+        Utils.prototype.createJSON = function (root) {
+            var action = {
+                type: root.type,
+                name: root.name,
+                detached: root.node.detached,
+                children: new Array(),
+                combine: new Array(),
+                properties: new Array()
+            };
+            // Set properties
+            for (var i = 0; i < root.properties.length; i++) {
+                action.properties.push({
+                    name: root.properties[i].text,
+                    value: root.propertiesResults[i].value,
+                    targetType: root.propertiesResults[i].targetType
+                });
+            }
+            // If combine
+            if (root.combineArray !== null) {
+                for (var i = 0; i < root.combineArray.length; i++) {
+                    var combinedAction = root.combineArray[i];
+                    action.combine.push(this.createJSON(combinedAction));
+                }
+                root = root.children[0]; // Hub
+            }
+            for (var i = 0; i < root.children.length; i++) {
+                action.children.push(this.createJSON(root.children[i]));
+            }
+            return action;
+        };
+        /*
+        *
+        */
+        Utils.prototype.setElementVisible = function (element, visible) {
+            element.style.display = visible ? "block" : "none";
+        };
+        return Utils;
+    })();
+    ActionsBuilder.Utils = Utils;
+})(ActionsBuilder || (ActionsBuilder = {}));

+ 646 - 0
Tools/ActionsBuilder/Sources/actionsbuilder.viewer.js

@@ -0,0 +1,646 @@
+var ActionsBuilder;
+(function (ActionsBuilder) {
+    var Viewer = (function () {
+        /*
+        * Constructor
+        * @param type: the root type object (OBJECT or SCENE)
+        */
+        function Viewer(type) {
+            var _this = this;
+            this.objectName = "Unnamed Object";
+            this.zoom = 1.0;
+            this._firstUpdate = true;
+            // Get HTML elements
+            this.viewerContainer = document.getElementById("GraphContainerID");
+            this.viewerElement = document.getElementById("GraphElementID");
+            // Create element
+            this.paper = Raphael("GraphElementID", screen.width, screen.height);
+            // Configure this
+            //var name = type === Type.OBJECT ? "Unnamed object" : "Scene";
+            this.root = this.addAction(null, type, { name: this.objectName, text: this.objectName, properties: [], description: "" });
+            this.selectedNode = null;
+            // Configure events
+            window.addEventListener("resize", function (event) {
+                _this.onResize(event);
+            });
+            window.addEventListener("mousemove", function (event) {
+                _this.onMove(event);
+            });
+            this.paper.canvas.addEventListener("click", function (event) {
+                _this.onClick(event);
+            });
+            // Load modules
+            this._toolbar = new ActionsBuilder.Toolbar(this);
+            this._contextMenu = new ActionsBuilder.ContextMenu(this);
+            this.parameters = new ActionsBuilder.Parameters(this);
+            this.utils = new ActionsBuilder.Utils(this);
+            // Finish
+            this.parameters.parametersHelpElement.textContent = Viewer._DEFAULT_INFO_MESSAGE;
+        }
+        Object.defineProperty(Viewer, "NODE_WIDTH", {
+            get: function () {
+                return Viewer._NODE_WIDTH;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Viewer, "NODE_HEIGHT", {
+            get: function () {
+                return Viewer._NODE_HEIGHT;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Viewer, "NODE_MINIMIZED_WIDTH", {
+            get: function () {
+                return Viewer._NODE_MINIMIZE_WIDTH;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Viewer, "VERTICAL_OFFSET", {
+            get: function () {
+                return Viewer._VERTICAL_OFFSET;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /*
+        * Resize event
+        * @param event: the resize event
+        */
+        Viewer.prototype.onResize = function (event) {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.viewerContainer.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 50 + "px";
+            this.viewerElement.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 50 + "px";
+            this.parameters.onResize();
+            this._toolbar.onResize();
+            if (this.paper.height < window.innerHeight) {
+                this.paper.setSize(this.paper.width, window.innerHeight);
+            }
+            if (this._firstUpdate) {
+                this.viewerElement.scrollLeft = ((this.viewerElement.scrollWidth / 2) - (this.viewerElement.getBoundingClientRect().width / 2));
+                this._firstUpdate = false;
+            }
+        };
+        /*
+        * Handles the onMove event
+        * @param event: the onMove mouse event
+        */
+        Viewer.prototype.onMove = function (event) {
+            this.mousex = event.clientX - this.paper.canvas.getBoundingClientRect().left;
+            this.mousey = event.clientY - this.paper.canvas.getBoundingClientRect().top;
+        };
+        /*
+        * Handles the onClick event to get selected node
+        * @param event: the onClick mouse event
+        */
+        Viewer.prototype.onClick = function (event) {
+            if (this._contextMenu.showing) {
+                return;
+            }
+            // Reset selected node
+            if (this.selectedNode !== null) {
+                var node = this.selectedNode.node;
+                node.rect.attr("fill", this.getNodeColor(this.selectedNode.type, node.detached));
+            }
+            // Configure new selected node
+            var result = this.traverseGraph(null, this.mousex, this.mousey, true);
+            if (result.hit) {
+                this.selectedNode = result.action;
+                var node = this.selectedNode.node;
+                node.rect.attr("fill", this.getSelectedNodeColor(this.selectedNode.type, node.detached));
+            }
+            else {
+                this.selectedNode = null;
+                this.parameters.clearParameters();
+                this.parameters.parametersHelpElement.textContent = Viewer._DEFAULT_INFO_MESSAGE;
+            }
+        };
+        /*
+        * Set the color theme of the viewer
+        * @param color: the color theme ( ex: "rgb(64, 64, 64)" )
+        */
+        Viewer.prototype.setColorTheme = function (color) {
+            this.paper.canvas.style.background = color;
+        };
+        /*
+        * Returns the color according to the given parameters
+        * @param action: the action used to select the color
+        * @param detached: if the node is attached to its parent or not
+        */
+        Viewer.prototype.getNodeColor = function (type, detached) {
+            if (detached) {
+                return Raphael.rgb(96, 122, 14);
+            }
+            switch (type) {
+                case ActionsBuilder.Type.TRIGGER:
+                    return Raphael.rgb(133, 154, 185);
+                    break;
+                case ActionsBuilder.Type.ACTION:
+                    return Raphael.rgb(182, 185, 132);
+                    break;
+                case ActionsBuilder.Type.FLOW_CONTROL:
+                    return Raphael.rgb(185, 132, 140);
+                    break;
+                case ActionsBuilder.Type.OBJECT:
+                case ActionsBuilder.Type.SCENE:
+                    return Raphael.rgb(255, 255, 255);
+                    break;
+                default: break;
+            }
+            return null;
+        };
+        /*
+        * Returns the selected node color according to the given parameters
+        * @param action: the action used to select the color
+        * @param detached: if the node is attached to its parent or not
+        */
+        Viewer.prototype.getSelectedNodeColor = function (type, detached) {
+            if (detached) {
+                return Raphael.rgb(96, 122, 14);
+            }
+            switch (type) {
+                case ActionsBuilder.Type.TRIGGER:
+                    return Raphael.rgb(41, 129, 255);
+                    break;
+                case ActionsBuilder.Type.ACTION:
+                    return Raphael.rgb(255, 220, 42);
+                    break;
+                case ActionsBuilder.Type.FLOW_CONTROL:
+                    return Raphael.rgb(255, 41, 53);
+                    break;
+                case ActionsBuilder.Type.OBJECT:
+                case ActionsBuilder.Type.SCENE:
+                    return Raphael.rgb(255, 255, 255);
+                    break;
+                default: break;
+            }
+            return null;
+        };
+        /*
+        * Removes the given action from the graph
+        * @param action: the action to remove
+        * @param removeChildren: if remove the branch or not
+        */
+        Viewer.prototype.removeAction = function (action, removeChildren) {
+            // If selected node is combine
+            if (action.parent !== null && action.parent.hub === action) {
+                this.removeAction(action.parent, false);
+                return;
+            }
+            // Basic suppress
+            this.removeNode(action.node);
+            if (action.combineArray !== null) {
+                this.removeNode(action.hub.node);
+                // Remove combine array
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    this.removeNode(action.combineArray[i].node);
+                }
+                action.combineArray.length = 0;
+            }
+            if (removeChildren) {
+                for (var i = 0; i < action.children.length; i++) {
+                    this.removeAction(action.children[i], removeChildren);
+                }
+                action.clearChildren();
+            }
+            else {
+                for (var i = 0; i < action.children.length; i++) {
+                    action.parent.addChild(action.children[i]);
+                    action.children[i].parent = action.parent;
+                }
+            }
+        };
+        /*
+        * Removes the given node (not the action)
+        * @param node: the node to remove
+        */
+        Viewer.prototype.removeNode = function (node) {
+            node.rect.remove();
+            node.text.remove();
+            if (node.line !== null) {
+                node.line.remove();
+            }
+        };
+        /*
+        * Updates the graph viewer
+        */
+        Viewer.prototype.update = function () {
+            var _this = this;
+            // Set root position
+            this._setActionPosition(this.root, (this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * this.zoom, 10);
+            // Sets node size
+            var onSetNodeSize = function (node) {
+                node.rect.attr("width", node.minimized ? Viewer.NODE_MINIMIZED_WIDTH : Viewer.NODE_WIDTH * _this.zoom);
+                node.rect.attr("height", Viewer.NODE_HEIGHT * _this.zoom);
+                node.text.attr("font-size", 11 * _this.zoom);
+            };
+            // First pass: set actions positions according to parents
+            var onSetPositionPass = function (action, yPosition) {
+                var node = action.node;
+                var parent = action.parent !== null ? action.parent : null;
+                // Set node properties (size, text size, etc.)
+                if (action.combineArray !== null) {
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        var combinedNode = action.combineArray[i].node;
+                        onSetNodeSize(combinedNode);
+                    }
+                }
+                onSetNodeSize(node);
+                // Set position from parent
+                if (parent) {
+                    var parentx = parent.node.rect.attr("x");
+                    if (parent.combineArray !== null && parent.combineArray.length > 1) {
+                        parentx += parent.node.rect.attr("width") / 2;
+                    }
+                    _this._setActionPosition(action, parentx, yPosition);
+                    _this._setActionLine(action);
+                }
+                // Calculate total width for current action
+                var totalSize = 0;
+                for (var i = 0; i < action.children.length; i++) {
+                    var childNode = action.children[i].node;
+                    totalSize += childNode.rect.attr("width");
+                }
+                // Get values to place nodes according to the parent position
+                var nodeWidth = node.rect.attr("width");
+                var startingPositionX = node.rect.attr("x");
+                // Set children positions
+                for (var i = 0; i < action.children.length; i++) {
+                    var childAction = action.children[i];
+                    var childNode = childAction.node;
+                    var newPositionX = startingPositionX;
+                    if (childAction.combineArray !== null && childAction.combineArray.length > 1) {
+                        newPositionX -= (childNode.rect.attr("width") / 2) - nodeWidth / 2;
+                    }
+                    var newPositionY = yPosition + Viewer.VERTICAL_OFFSET * _this.zoom;
+                    onSetPositionPass(childAction, newPositionY);
+                    _this._setActionPosition(childAction, newPositionX, newPositionY);
+                    _this._setActionLine(childAction);
+                }
+            };
+            onSetPositionPass(this.root, 10 * this.zoom);
+            // Seconds pass, get sizes of groups
+            var onGetSizePass = function (action, maxSize) {
+                var mySize = 0;
+                if (action.combineArray !== null) {
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        mySize += action.combineArray[i].node.rect.attr("width");
+                    }
+                }
+                else {
+                    mySize = action.node.rect.attr("width");
+                }
+                if (mySize > maxSize) {
+                    maxSize = mySize;
+                }
+                for (var i = 0; i < action.children.length; i++) {
+                    maxSize = onGetSizePass(action.children[i], maxSize);
+                }
+                return maxSize;
+            };
+            // Resize canvas
+            var onResizeCanvas = function (action) {
+                var node = action.node;
+                var nodex = node.rect.attr("x");
+                var nodey = node.rect.attr("y");
+                if (nodex < 0 || nodex > _this.paper.width) {
+                    _this.paper.setSize(_this.paper.width + 1000, _this.paper.height);
+                    _this._setActionPosition(_this.root, (_this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * _this.zoom, 10);
+                }
+                if (nodey > _this.paper.height) {
+                    _this.paper.setSize(_this.paper.width, _this.paper.height + 1000);
+                    _this._setActionPosition(_this.root, (_this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * _this.zoom, 10);
+                }
+            };
+            var widths = new Array();
+            for (var i = 0; i < this.root.children.length; i++) {
+                var trigger = this.root.children[i];
+                var triggerResult = { triggerWidth: onGetSizePass(trigger, 0), childrenWidths: new Array() };
+                if (trigger.children.length > 0) {
+                    triggerResult.triggerWidth = 0;
+                }
+                for (var j = 0; j < trigger.children.length; j++) {
+                    var actionWidth = onGetSizePass(trigger.children[j], 0);
+                    triggerResult.triggerWidth += actionWidth + 15;
+                    triggerResult.childrenWidths.push({
+                        triggerWidth: actionWidth,
+                        childrenWidths: null
+                    });
+                }
+                widths.push(triggerResult);
+            }
+            // Third pass, set positions of nodes
+            var onSetNodePosition = function (action, widthArray, isChild) {
+                var actionsCount = action.children.length;
+                var actionsMiddle = actionsCount % 2;
+                var actionsHasMiddle = actionsMiddle !== 0;
+                var actionsLeftOffset = 0;
+                var actionsRightOffset = 0;
+                var actionWidth = action.node.rect.attr("width");
+                if (actionsHasMiddle && actionsCount > 1) {
+                    var middle = Math.floor(actionsCount / 2);
+                    actionsLeftOffset += widthArray[middle].triggerWidth / 2;
+                    actionsRightOffset += widthArray[middle].triggerWidth / 2;
+                }
+                // Move left
+                var leftStart = actionsHasMiddle ? Math.floor(actionsCount / 2) - 1 : (actionsCount / 2) - 1;
+                for (var i = leftStart; i >= 0; i--) {
+                    var child = action.children[i];
+                    var node = child.node;
+                    var width = (widthArray[i].triggerWidth) + 15;
+                    _this._setActionPosition(action.children[i], node.rect.attr("x") - actionsLeftOffset - (width / 2), node.rect.attr("y"));
+                    _this._setActionLine(child);
+                    onResizeCanvas(child);
+                    actionsLeftOffset += width;
+                }
+                // Move right
+                var rightStart = actionsHasMiddle ? Math.round(actionsCount / 2) : actionsCount / 2;
+                for (var i = rightStart; i < actionsCount; i++) {
+                    var child = action.children[i];
+                    var node = child.node;
+                    var width = (widthArray[i].triggerWidth) + 15;
+                    _this._setActionPosition(action.children[i], node.rect.attr("x") + actionsRightOffset + (width / 2), node.rect.attr("y"));
+                    _this._setActionLine(child);
+                    onResizeCanvas(child);
+                    actionsRightOffset += width;
+                }
+            };
+            onSetNodePosition(this.root, widths, false);
+            for (var i = 0; i < this.root.children.length; i++) {
+                onSetNodePosition(this.root.children[i], widths[i].childrenWidths, true);
+            }
+        };
+        /*
+        * Adds an action to the graph viewer and returns it
+        * @param parent: the parent action
+        * @param type: the action type
+        * @param element: the Actions Builder type (TRIGGERS, ACTIONS, FLOW_CONTROLS)
+        */
+        Viewer.prototype.addAction = function (parent, type, element) {
+            var node = this._createNode(element.text, type, parent === null);
+            var action = new ActionsBuilder.Action(node);
+            if (element.name === "CombineAction") {
+                action.combineArray = new Array();
+                var hubElement = ActionsBuilder.Elements.FLOW_CONTROLS[ActionsBuilder.Elements.FLOW_CONTROLS.length - 1];
+                var hub = this.addAction(action, ActionsBuilder.Type.FLOW_CONTROL, hubElement);
+                action.hub = hub;
+                action.addChild(hub);
+                this._createActionAnimation(hub);
+            }
+            action.name = element.name;
+            action.properties = element.properties;
+            action.type = type;
+            // Configure properties
+            for (var i = 0; i < action.properties.length; i++) {
+                action.propertiesResults.push({ targetType: action.properties[i].targetType, value: action.properties[i].value });
+            }
+            if (action.properties !== null && action.properties.length > 0) {
+                if (action.properties[0].text === "target") {
+                    action.propertiesResults[0].value = this.objectName;
+                }
+            }
+            if (parent !== null) {
+                if (parent.combineArray === null) {
+                    parent.addChild(action);
+                }
+                else if (parent.combineArray !== null && action.name !== "Hub") {
+                    parent.combineArray.push(action);
+                    action.parent = parent;
+                    action.combineAction = parent;
+                    parent.node.text.attr("text", "");
+                }
+            }
+            // Create animation
+            this._createActionAnimation(action);
+            return action;
+        };
+        /*
+        * Traverses the graph viewer and returns if an action
+        * is selected at coordinates (x, y)
+        * @param start: the start node. Can be null
+        * @param x: the x coordinate
+        * @param y: the y coordinate
+        * @param traverseCombine: if we traverse combine actions children
+        */
+        Viewer.prototype.traverseGraph = function (start, x, y, traverseCombine) {
+            if (start === null)
+                start = this.root;
+            var result = { action: start, hit: true };
+            if (start.node.isPointInside(x, y)) {
+                return result;
+            }
+            for (var i = 0; i < start.children.length; i++) {
+                var action = start.children[i];
+                if (action.node.isPointInside(x, y)) {
+                    result.hit = true;
+                    result.action = start.children[i];
+                    if (traverseCombine && action.combineArray !== null) {
+                        for (var j = 0; j < action.combineArray.length; j++) {
+                            if (action.combineArray[j].node.isPointInside(x, y)) {
+                                result.action = action.combineArray[j];
+                                break;
+                            }
+                        }
+                    }
+                    return result;
+                }
+                result = this.traverseGraph(action, x, y, traverseCombine);
+                if (result.hit) {
+                    return result;
+                }
+            }
+            result.hit = false;
+            result.action = null;
+            return result;
+        };
+        /*
+        * Sets the action's position (node)
+        * @param action: the action to place
+        * @param x: the x position of the action
+        * @param y: the y position of the action
+        */
+        Viewer.prototype._setActionPosition = function (action, x, y) {
+            var node = action.node;
+            var offsetx = node.rect.attr("x") - x;
+            var parent = action.parent;
+            if (parent !== null && parent.combineArray !== null && parent.combineArray.length > 1) {
+                var parentNode = parent.node;
+                x = parentNode.rect.attr("x") + (parent.node.rect.attr("width") / 2) - (node.rect.attr("width") / 2);
+            }
+            node.rect.attr("x", x);
+            node.rect.attr("y", y);
+            var textBBox = node.text.getBBox();
+            var textWidth = 0;
+            if (textBBox !== null && textBBox !== undefined) {
+                textWidth = textBBox.width;
+            }
+            node.text.attr("x", x + node.rect.attr("width") / 2 - textWidth / 2);
+            node.text.attr("y", y + node.rect.attr("height") / 2);
+            if (action.combineArray !== null && action.combineArray.length > 0) {
+                var length = 0;
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    var combinedAction = action.combineArray[i];
+                    var combinedNode = combinedAction.node;
+                    combinedNode.rect.attr("x", node.rect.attr("x") + length);
+                    combinedNode.rect.attr("y", node.rect.attr("y"));
+                    textBBox = combinedNode.text.getBBox();
+                    if (textBBox !== null) {
+                        textWidth = textBBox.width;
+                    }
+                    combinedNode.text.attr("x", combinedNode.rect.attr("x") + combinedNode.rect.attr("width") / 2 - textWidth / 2);
+                    combinedNode.text.attr("y", y + combinedNode.rect.attr("height") / 2);
+                    length += combinedNode.rect.attr("width");
+                }
+                node.rect.attr("width", length);
+            }
+            for (var i = 0; i < action.children.length; i++) {
+                var child = action.children[i];
+                this._setActionPosition(child, child.node.rect.attr("x") - offsetx, y + Viewer.VERTICAL_OFFSET * this.zoom);
+                this._setActionLine(child);
+            }
+        };
+        /*
+        * Configures the line (link) between the action and its parent
+        * @param action: the action to configure
+        */
+        Viewer.prototype._setActionLine = function (action) {
+            if (action.node.line === null) {
+                return;
+            }
+            var node = action.node;
+            var nodex = node.rect.attr("x");
+            var nodey = node.rect.attr("y");
+            var nodeWidth = node.rect.attr("width");
+            var nodeHeight = node.rect.attr("height");
+            var parent = action.parent.node;
+            var parentx = parent.rect.attr("x");
+            var parenty = parent.rect.attr("y");
+            var parentWidth = parent.rect.attr("width");
+            var parentHeight = parent.rect.attr("height");
+            if (node.detached) {
+                node.line.attr("path", ["M", nodex, nodey, "L", nodex, nodey]);
+                return;
+            }
+            var line1x = nodex + (nodeWidth / 2);
+            var line1y = nodey;
+            var line2y = line1y - (line1y - parenty - parentHeight) / 2;
+            var line3x = parentx + (parentWidth / 2);
+            var line4y = parenty + parentHeight;
+            node.line.attr("path", ["M", line1x, line1y, "L", line1x, line2y, "L", line3x, line2y, "L", line3x, line4y]);
+        };
+        /*
+        * Creates and returns a node
+        * @param text: the text to draw in the nde
+        * @param color: the node's color
+        * @param noLine: if draw a line to the parent or not
+        */
+        Viewer.prototype._createNode = function (text, type, noLine) {
+            var node = new ActionsBuilder.Node();
+            var color = this.getNodeColor(type, false);
+            node.rect = this.paper.rect(20, 20, Viewer.NODE_WIDTH, Viewer.NODE_HEIGHT, 0);
+            node.rect.attr("fill", color);
+            node.text = this.paper.text(20, 20, text);
+            node.text.attr("font-size", 11);
+            node.text.attr("text-anchor", "start");
+            node.text.attr("font-family", "Sinkin Sans Light");
+            if (!noLine) {
+                node.line = this.paper.path("");
+                node.line.attr("stroke", color);
+            }
+            return node;
+        };
+        /*
+        * Creates the drag animation
+        * @param action: the action to animate
+        */
+        Viewer.prototype._createActionAnimation = function (action) {
+            var _this = this;
+            var node = action.node;
+            var finished = true;
+            var nodex = 0;
+            var nodey = 0;
+            var onMove = function (dx, dy, x, y) { };
+            var onStart = function (x, y, event) {
+                if (node.minimized) {
+                    return;
+                }
+                if (finished) {
+                    nodex = node.rect.attr("x");
+                    nodey = node.rect.attr("y");
+                }
+                finished = false;
+                node.rect.animate({
+                    x: node.rect.attr("x") - 10,
+                    y: node.rect.attr("y"),
+                    width: (Viewer.NODE_WIDTH + 20) * _this.zoom,
+                    height: (Viewer.NODE_HEIGHT + 10) * _this.zoom,
+                    opacity: 0.25
+                }, 500, ">");
+            };
+            var onEnd = function (event) {
+                if (!node.minimized) {
+                    node.rect.animate({
+                        x: nodex,
+                        y: nodey,
+                        width: Viewer.NODE_WIDTH * _this.zoom,
+                        height: Viewer.NODE_HEIGHT * _this.zoom,
+                        opacity: 1.0
+                    }, 500, ">", function () { finished = true; });
+                }
+                var dragResult = _this.traverseGraph(null, _this.mousex, _this.mousey, true);
+                if (dragResult.hit && dragResult.action === action || !dragResult.hit) {
+                    // Create parameters. Action can be null
+                    _this.parameters.createParameters(action);
+                }
+                else {
+                    // Manage drag'n'drop
+                    if (dragResult.action.children.length > 0 && action.type !== ActionsBuilder.Type.TRIGGER) {
+                        return;
+                    }
+                    if (action.type === ActionsBuilder.Type.TRIGGER && dragResult.action !== _this.root) {
+                        return;
+                    }
+                    if (action.type === ActionsBuilder.Type.ACTION && dragResult.action === _this.root) {
+                        return;
+                    }
+                    if (action.type === ActionsBuilder.Type.FLOW_CONTROL && (dragResult.action === _this.root || dragResult.action.type === ActionsBuilder.Type.FLOW_CONTROL)) {
+                        return;
+                    }
+                    if (action === dragResult.action.parent) {
+                        return;
+                    }
+                    if (action.parent !== null && action.parent.combineArray !== null) {
+                        return;
+                    }
+                    // Reset node
+                    node.rect.stop(node.rect.animation);
+                    node.text.stop(node.text.animation);
+                    node.rect.undrag();
+                    node.text.undrag();
+                    node.rect.attr("opacity", 1.0);
+                    node.rect.attr("width", Viewer.NODE_WIDTH);
+                    node.rect.attr("height", Viewer.NODE_HEIGHT);
+                    if (action.parent !== null) {
+                        // Configure drag'n'drop
+                        action.parent.removeChild(action);
+                        dragResult.action.addChild(action);
+                        _this.update();
+                        _this._createActionAnimation(action);
+                    }
+                }
+            };
+            node.rect.drag(onMove, onStart, onEnd);
+            node.text.drag(onMove, onStart, onEnd);
+        };
+        // Statics
+        Viewer._NODE_WIDTH = 150;
+        Viewer._NODE_HEIGHT = 25;
+        Viewer._NODE_MINIMIZE_WIDTH = 50;
+        Viewer._VERTICAL_OFFSET = 70;
+        Viewer._DEFAULT_INFO_MESSAGE = "Select or add a node to customize actions";
+        return Viewer;
+    })();
+    ActionsBuilder.Viewer = Viewer;
+})(ActionsBuilder || (ActionsBuilder = {}));

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 34265 - 0
Tools/ActionsBuilder/Sources/babylon.max.js


+ 36 - 0
Tools/ActionsBuilder/Sources/fonts.css

@@ -0,0 +1,36 @@
+@font-face {
+    font-family: "Sinkin Sans Light";
+    src: url('fonts/SinkinSans/SinkinSans-300Light.ttf');
+    src: url('fonts/SinkinSans/SinkinSans-300Light.eot?#iefix') format('embedded-opentype'),
+        url('fonts/SinkinSans/SinkinSans-300Light.woff') format('woff'),
+        url('fonts/SinkinSans/SinkinSans-300Light.ttf') format('truetype'),
+        url('fonts/SinkinSans/SinkinSans-300Light.svg#segeoN') format('svg');
+}
+
+@font-face {
+    font-family: "Sinkin Sans Light Italic";
+    src: url('fonts/SinkinSans/SinkinSans-300LightItalic.ttf');
+    src: url('fonts/SinkinSans/SinkinSans-300LightItalic.eot?#iefix') format('embedded-opentype'),
+        url('fonts/SinkinSans/SinkinSans-300LightItalic.woff') format('woff'),
+        url('fonts/SinkinSans/SinkinSans-300LightItalic.ttf') format('truetype'),
+        url('fonts/SinkinSans/SinkinSans-300LightItalic.svg#segeoN') format('svg');
+}
+
+@font-face {
+    font-family: "Sinkin Sans Medium";
+    src: url('fonts/SinkinSans/SinkinSans-500Medium.ttf');
+    src: url('fonts/SinkinSans/SinkinSans-500Medium.eot?#iefix') format('embedded-opentype'),
+        url('fonts/SinkinSans/SinkinSans-500Medium.woff') format('woff'),
+        url('fonts/SinkinSans/SinkinSans-500Medium.ttf') format('truetype'),
+        url('fonts/SinkinSans/SinkinSans-500Medium.svg#segeoN') format('svg');
+}
+
+@font-face {
+    font-family: "Sinkin Sans Semi-Bold";
+    src: url('fonts/SinkinSans/SinkinSans-600SemiBold.ttf');
+    src: url('fonts/SinkinSans/SinkinSans-600SemiBold.eot?#iefix') format('embedded-opentype'),
+        url('fonts/SinkinSans/SinkinSans-600SemiBold.woff') format('woff'),
+        url('fonts/SinkinSans/SinkinSans-600SemiBold.ttf') format('truetype'),
+        url('fonts/SinkinSans/SinkinSans-600SemiBold.svg#segeoN') format('svg');
+}
+

BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.eot


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.otf


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2984 - 0
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.svg


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.ttf


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300Light.woff


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.eot


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.otf


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2997 - 0
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.svg


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.ttf


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-300LightItalic.woff


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.eot


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.otf


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2990 - 0
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.svg


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.ttf


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-500Medium.woff


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.eot


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.otf


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3016 - 0
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.svg


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.ttf


BIN
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans-600SemiBold.woff


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 91 - 0
Tools/ActionsBuilder/Sources/index-debug.html


+ 191 - 0
Tools/ActionsBuilder/Sources/index.css

@@ -0,0 +1,191 @@
+html, body {
+  width: 100%;
+  height: 100%;
+
+  max-width: 100%;
+  max-height: 100%;
+
+  /*background: rgb(64, 64, 64);*/
+
+  overflow: hidden;
+  font-family: "Sinkin Sans Light";
+}
+
+* {
+  position: relative;
+}
+
+#ParentContainerID {
+    height: 100%;
+    width: 100%;
+
+    max-height: 100%;
+    max-width: 100%;
+
+    display: inline-block;
+}
+
+#ListsElementID {
+    width: 25%;
+    height: 100%;
+    
+    max-width: 25%;
+    max-height: 100%;
+
+    background: rgb(64, 64, 64);
+
+    float: left;
+    overflow-y: scroll;
+    overflow-x: hidden;
+}
+
+#GraphContainerID {
+    width: 50%;
+    max-width: 50%;
+
+    float: left;
+    position: relative;
+}
+
+#GraphElementID {
+    position: absolute;
+
+    width: 100%;
+    height: 100%;
+
+    overflow-y: auto;
+    overflow-x: auto;
+
+    max-width: 100%;
+    max-height: 100%;
+}
+
+#ToolbarElementID {
+    position: absolute;
+
+    width: 100%;
+    height: 42px;
+
+    max-width: 100%;
+    max-height: 42px;
+
+    float: left;
+    background: rgb(0, 0, 0);
+
+    vertical-align: middle;
+}
+
+#ToolbarElementID li {
+    color: white;
+    margin-left: 2em;
+    margin-top: 1em;
+}
+
+#ParametersContainerID {
+    width: 25%;
+    height: 100%;
+
+    max-width: 25%;
+    max-height: 100%;
+
+    float: right;
+    overflow-y: auto;
+}
+
+#ParametersElementID {
+    width: 100%;
+    max-width: 100%;
+
+    background: rgb(32, 32, 32);
+    overflow: auto;
+}
+
+#ParametersHelpElementID {
+    background: rgb(0, 0, 0);
+
+    font-family: "Sinkin Sans Light";
+    font-size: 12px;
+    font-style: italic;
+
+    text-align: center;
+    text-anchor: middle;
+    color: white;
+}
+
+.ParametersElementNodeClass {
+    font-size: 11px;
+    height: 25px;
+    margin-top: 10px;
+    border: 1px solid white;
+
+    text-align: center;
+
+    margin-left: auto;
+    margin-right: auto;
+    width: 90%;
+}
+.ParametersElementNodeTextClass {
+    width: 100%;
+    height: 100%;
+
+    vertical-align: middle;
+    line-height: 25px;
+}
+.ParametersElementSeparatorClass {
+    width: 90%;
+}
+.ParametersElementTitleClass {
+    margin-left: 3em;
+    display: block;
+    color: white;
+    font-size: 11px;
+}
+.ParametersElementSelectClass {
+    margin-top: 15px;
+
+    width: 80%;
+    height: 25px;
+
+    display: block;
+    font-size: 11px;
+    margin-left: 3em;
+}
+.ParametersElementInputClass {
+    margin-top: 15px;
+
+    width: 80%;
+    height: 15px;
+
+    display: block;
+    font-size: 11px;
+    margin-left: 3em;
+}
+
+/*
+UL & LI (toolbar & top buttons)
+*/
+#ToolsButtonsID {
+    position: relative;
+
+    width: 100%;
+    height: 25px;
+
+    max-width: 100%;
+    max-height: 25px;
+}
+
+ul {
+  padding-left: 1em;
+  margin-bottom: 0.5em;
+  margin-top: 0.0em;
+}
+ul li {
+  list-style-type: none;
+  display: inline-block;
+  margin-right: 3em;
+  cursor: pointer;
+  font-size: 12px;
+}
+ul li:last-child {
+  margin-right: 0;
+}

+ 72 - 0
Tools/ActionsBuilder/Sources/index.html

@@ -0,0 +1,72 @@
+<!-- saved from url=(0014)about:internet -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <title>Actions Builder</title>
+
+    <!-- CSS FILES -->
+    <link href="./index.css" rel="stylesheet" />
+    <link href="./fonts.css" rel="stylesheet" />
+
+</head>
+
+<body>
+
+    <!-- PARENT CONTAINER -->
+    <div id="ParentContainerID">
+
+        <ul id="ToolsButtonsID">
+            <li id="ToolsButtonIDSaveActionGraph">save action graph</li>
+            <li id="ResetActionGraphID">reset action graph</li>
+            <li id="TestActionGraphID">test action graph</li>
+        </ul>
+
+        <!-- LIST CONTAINER -->
+        <div id="ListsElementID">
+            <div id="TriggersListID"></div>
+            <div id="ActionsListID"></div>
+            <div id="FlowActionsListID"></div>
+        </div>
+
+        <!-- GRAPH CONTAINER -->
+        <div id="GraphContainerID">
+            <div id="GraphElementID"></div>
+
+            <div id="ToolbarElementID">
+                <ul>
+                    <li id="ViewerReduceAll">reduce all</li>
+                    <li id="ViewerExpandAll">expand all</li>
+                    <li id="ViewerDisconnectAll">disconnect all</li>
+                    <li id="ViewerReconnectAll">reconnect all</li>
+                    <li class="border" style="width: 15px; text-align: center" id="ViewerDeZoomID"> - </li>
+                    <li class="border" style="width: 15px; text-align: center" id="ViewerZoomID"> + </li>
+                </ul>
+            </div>
+        </div>
+
+        <!-- PARAMETERS CONTAINER -->
+        <div id="ParametersContainerID">
+            <div id="ParametersElementID"></div>
+            <div id="ParametersHelpElementID"></div>
+        </div>
+
+    </div>
+
+    <canvas id="RenderCanvasID" style="display:none;"></canvas>
+    <input id="ActionsBuilderObjectName" type="hidden" />
+    <input id="ActionsBuilderJSON" type="hidden" />
+
+    <!-- JS FILES -->
+    <script src="raphael.js" type="text/javascript"></script>
+    <script src="babylon.max.js" type="text/javascript"></script>
+    <script src="actionsbuilder.max.js" type="text/javascript"></script>
+
+    <script type="text/javascript">
+        run();
+    </script>
+
+</body>
+
+</html>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 11 - 0
Tools/ActionsBuilder/Sources/raphael.js


+ 101 - 0
Tools/ActionsBuilder/actionsbuilder.actionNode.ts

@@ -0,0 +1,101 @@
+module ActionsBuilder {
+    export class Node {
+        public rect: Rect = null;
+        public text: Text = null;
+        public line: Path = null;
+
+        public detached: boolean = false;
+        public minimized: boolean = false;
+
+        constructor()
+        { }
+
+        /**
+        * Returns if the point (x, y) is inside the text or rect
+        * @param x: the x position of the point
+        * @param y: the y position of the point
+        */
+        public isPointInside(x: number, y: number): boolean {
+            return this.rect.isPointInside(x, y) || this.text.isPointInside(x, y);
+        }
+    }
+
+    export class Action {
+        public node: Node;
+        public parent: Action = null;
+        public children: Array<Action> = new Array<Action>();
+
+        public name: string = "";
+        public type: number = Type.OBJECT;
+
+        public properties: Array<ElementProperty> = new Array<ElementProperty>();
+        public propertiesResults: Array<ElementPropertyResult> = new Array<ElementPropertyResult>();
+
+        public combineArray: Array<Action> = null;
+        public hub: Action = null;
+        public combineAction: Action = null;
+
+        /**
+        * Constructor
+        * @param node: The associated node to draw in the viewer
+        */
+        constructor(node: Node) {
+            this.node = node;
+        }
+        
+        /*
+        * Removes a combined action from the combine array
+        * @param action: the action to remove
+        */
+        public removeCombinedAction(action: Action): boolean {
+            if (action === null || this.combineArray === null) {
+                return false;
+            }
+
+            var index = this.combineArray.indexOf(action);
+            if (index !== -1) {
+                this.combineArray.splice(index, 1);
+            }
+
+            return false;
+        }
+
+        /*
+        * Adds a child
+        * @param child: the action to add as child
+        */
+        public addChild(child: Action): boolean {
+            if (child === null) {
+                return false;
+            }
+
+            this.children.push(child);
+            child.parent = this;
+
+            return true;
+        }
+
+        /*
+        * Removes the given action to children
+        * @param child: the child to remove
+        */
+        public removeChild(child: Action): boolean {
+            var indice = this.children.indexOf(child);
+
+            if (indice !== -1) {
+                this.children.splice(indice, 1);
+                return true;
+            }
+
+            return false;
+        }
+
+        /*
+        * Clears the children's array
+        */
+        public clearChildren(): void {
+            this.children = new Array<Action>();
+        }
+
+    }
+}

+ 149 - 0
Tools/ActionsBuilder/actionsbuilder.contextMenu.ts

@@ -0,0 +1,149 @@
+module ActionsBuilder {
+
+    export interface ContextMenuElement {
+        text: string;
+        node: Node;
+        action: string;
+    }
+
+    export class ContextMenu {
+        public showing: boolean = false;
+        public savedColor: RaphaelColor = Raphael.rgb(255, 255, 255);
+        public overColor: RaphaelColor = Raphael.rgb(140, 200, 230);
+
+        private _viewer: Viewer = null;
+        private elements: Array<ContextMenuElement> = [
+            { text: "Reduce", node: null, action: "onReduce" },
+            { text: "Delete", node: null, action: "onRemoveNode" },
+            { text: "Delete branch", node: null, action: "onRemoveBranch" },
+            { text: "Connect / Disconnect", node: null, action: "onDetachAction" },
+            { text: "Copy", node: null, action: "onCopyStructure" },
+            { text: "Paste", node: null, action: "onPasteStructure" },
+            // Add other elements here
+            { text: "", node: null, action: null } // Color separator (top)
+        ];
+
+        /*
+        * Constructor
+        * @param viewer: the graph viewer
+        */
+        constructor(viewer: Viewer) {
+            // Members
+            this._viewer = viewer;
+
+            // Configure
+            this.attachControl(this._viewer.paper.canvas);
+        }
+
+        public attachControl(element: HTMLElement): void {
+            var onClick = (event: MouseEvent) => {
+                var x = this._viewer.mousex;
+                var y = this._viewer.mousey;
+
+                // Remove all context menu nodes, and run action if selected
+                if (this.showing) {
+                    for (var i = 0; i < this.elements.length; i++) {
+                        var element = this.elements[i];
+
+                        if (element.action && element.node.rect.isPointInside(x, y)) {
+                            this._viewer.utils[element.action]();
+                            this._viewer.update();
+                        }
+
+                        element.node.rect.remove();
+                        element.node.text.remove();
+                    }
+                }
+                this.showing = false;
+            };
+
+            var onMouseMove = (event: MouseEvent) => {
+                // Override context menu's node color if mouse is inside
+                if (this.showing) {
+                    for (var i = 0; i < this.elements.length; i++) {
+                        var element = this.elements[i];
+
+                        if (element.text === "")
+                            continue;
+
+                        var x = this._viewer.mousex;
+                        var y = this._viewer.mousey;
+
+                        if (element.node.rect.isPointInside(x, y)) {
+                            element.node.rect.attr("fill", this.overColor);
+                        }
+                        else {
+                            element.node.rect.attr("fill", this.savedColor);
+                        }
+                    }
+                }
+            };
+
+            var onRightClick = (event: MouseEvent) => {
+                var x = this._viewer.mousex;
+                var y = this._viewer.mousey;
+
+                this._viewer.onClick(event);
+
+                // Set selected node
+                var result = this._viewer.traverseGraph(null, x, y, true);
+                if (result.hit) {
+                    //this._viewer.selectedNode = result.element;
+                }
+
+                // Properly draw the context menu on the screen
+                if (y + (Viewer.NODE_HEIGHT * this.elements.length) > this._viewer.viewerElement.offsetHeight + this._viewer.viewerElement.scrollTop) {
+                    y = (Viewer.NODE_HEIGHT * this.elements.length);
+                }
+                if (x + Viewer.NODE_WIDTH > this._viewer.viewerElement.offsetWidth + this._viewer.viewerElement.scrollLeft) {
+                    x -= Viewer.NODE_WIDTH;
+                }
+
+                if (!this.showing) {
+                    if (this._viewer.selectedNode === null)
+                        return;
+
+                    // Create elements
+                    var yOffset = 10;
+
+                    for (var i = 0; i < this.elements.length - 1; i++) {
+                        var element = this.elements[i];
+
+                        element.node = this._viewer._createNode(element.text, Type.OBJECT, true);
+                        element.node.rect.attr("fill", Raphael.rgb(216, 216, 216));
+
+                        element.node.rect.attr("x", x);
+                        element.node.rect.attr("y", y + yOffset);
+
+                        element.node.text.attr("x", x + 5);
+                        element.node.text.attr("y", y + yOffset + element.node.rect.attr("height") / 2);
+
+                        yOffset += Viewer.NODE_HEIGHT;
+                    }
+
+                    // Color separator
+                    var separator = this.elements[this.elements.length - 1];
+                    separator.node = this._viewer._createNode("", Type.OBJECT, true);
+                    separator.node.rect.attr("fill", this._viewer.getNodeColor(this._viewer.selectedNode.type, this._viewer.selectedNode.node.detached));
+
+                    separator.node.rect.attr("x", x);
+                    separator.node.rect.attr("y", y);
+                    separator.node.rect.attr("height", 10);
+
+                    // Finish
+                    this.showing = true;
+                }
+                else {
+                    onClick(event);
+                    onRightClick(event);
+                }
+
+                window.event.returnValue = false;
+            };
+
+            document.addEventListener("click", onClick);
+            document.addEventListener("mousemove", onMouseMove);
+            element.addEventListener("contextmenu", onRightClick);
+        }
+    }
+} 

+ 301 - 0
Tools/ActionsBuilder/actionsbuilder.list.ts

@@ -0,0 +1,301 @@
+module ActionsBuilder {
+    export class ListElement {
+        public rect: Rect = null;
+        public text: Text = null;
+        public name: string = "";
+        public type: number = Type.TRIGGER;
+        public element: Element = null;
+    }
+
+    export class List {
+        public listElement: HTMLElement;
+        public triggersElement: HTMLElement;
+        public actionsElement: HTMLElement;
+        public flowControlsElement: HTMLElement;
+
+        public triggersList: Paper;
+        public actionsList: Paper;
+        public flowControlsList: Paper;
+
+        private _parentContainer: HTMLElement;
+        private _listElements: Array<ListElement> = new Array<ListElement>();
+        private _viewer: Viewer;
+
+        public static get ELEMENT_HEIGHT(): number {
+            return 25;
+        }
+
+        /**
+        * Constructor
+        */
+        constructor(viewer: Viewer) {
+            // Get HTML elements
+            this.listElement = document.getElementById("ListsElementID");
+            this.triggersElement = document.getElementById("TriggersListID");
+            this.actionsElement = document.getElementById("ActionsListID");
+            this.flowControlsElement = document.getElementById("FlowActionsListID");
+            this._parentContainer = document.getElementById("ParentContainerID");
+
+            // Configure this
+            this._viewer = viewer;
+
+            // Create elements (lists)
+            this.triggersList = Raphael("TriggersListID", (25 * screen.width) / 100, 400);
+            this.actionsList = Raphael("ActionsListID", (25 * screen.width) / 100, 400);
+            this.flowControlsList = Raphael("FlowActionsListID", (25 * screen.width) / 100, 400);
+
+            // Manage events
+            window.addEventListener("resize", (event: Event) => {
+                this.onResize(event);
+            });
+        }
+
+        /**
+        * Resize event that resizes the list element dynamically
+        * @param event: the resize event
+        */
+        public onResize(event?: Event): void {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.listElement.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 + "px";
+
+            var listElementWidth = this.listElement.getBoundingClientRect().width;
+            for (var i = 0; i < this._listElements.length; i++) {
+                var rect = this._listElements[i].rect;
+                rect.attr("width", listElementWidth - 40);
+            }
+
+            this.triggersList.setSize(listElementWidth, this.triggersList.height);
+            this.actionsList.setSize(listElementWidth, this.triggersList.height);
+            this.flowControlsList.setSize(listElementWidth, this.triggersList.height);
+
+        }
+
+        public createListsElements(): void {
+            var excludedTriggers = [6, 9, 10];
+            var yPosition = 10;
+            var textColor = Raphael.rgb(61, 72, 76);
+            var whiteColor = Raphael.rgb(255, 255, 255);
+
+            var configureTitle = (listElement: ListElement, rectColor: RaphaelColor) => {
+                listElement.text.attr("x", 15);
+                listElement.rect.attr("fill", rectColor);
+                listElement.text.attr("font-family", "Sinkin Sans Medium");
+                listElement.text.attr("font-size", "11");
+            };
+
+            // Create triggers
+            var triggers = this._createListElement(this.triggersList, yPosition, "TRIGGERS", Type.TRIGGER, whiteColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(triggers, Raphael.rgb(41, 129, 255));
+
+            for (var i = 0; i < Elements.TRIGGERS.length; i++) {
+                var element: any = Elements.TRIGGERS[i];
+
+                if (this._viewer.root.type === Type.OBJECT && excludedTriggers.indexOf(i) !== -1) {
+                    continue;
+                }
+                var trigger = this._createListElement(this.triggersList, yPosition, element.text, Type.TRIGGER, textColor, true, element);
+
+                trigger.rect.attr("fill", Raphael.rgb(133, 154, 185));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+
+            yPosition += List.ELEMENT_HEIGHT;
+            this.triggersElement.style.height = this.triggersList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.triggersList, this.triggersElement, triggers, yPosition);
+
+            // Create actions
+            yPosition = 10;
+            var actions = this._createListElement(this.actionsList, yPosition, "ACTIONS", Type.ACTION, textColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(actions, Raphael.rgb(255, 220, 42));
+
+            for (var i = 0; i < Elements.ACTIONS.length; i++) {
+                var element: any = Elements.ACTIONS[i];
+                var action = this._createListElement(this.actionsList, yPosition, element.text, Type.ACTION, textColor, true, element);
+
+                action.rect.attr("fill", Raphael.rgb(182, 185, 132));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+
+            yPosition += List.ELEMENT_HEIGHT;
+            this.actionsElement.style.height = this.actionsList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.actionsList, this.actionsElement, actions, yPosition);
+
+            // Create flow controls
+            yPosition = 10;
+            var flowControls = this._createListElement(this.flowControlsList, yPosition, "FLOW CONTROLS", Type.FLOW_CONTROL, whiteColor, false);
+            yPosition += List.ELEMENT_HEIGHT;
+            configureTitle(flowControls, Raphael.rgb(255, 41, 53));
+
+            for (var i = 0; i < Elements.FLOW_CONTROLS.length - 1; i++) {
+                var element: any = Elements.FLOW_CONTROLS[i];
+                var flowControl = this._createListElement(this.flowControlsList, yPosition, element.text, Type.FLOW_CONTROL, textColor, true, element);
+
+                flowControl.rect.attr("fill", Raphael.rgb(185, 132, 140));
+                yPosition += List.ELEMENT_HEIGHT;
+            }
+
+            yPosition += List.ELEMENT_HEIGHT;
+            this.flowControlsElement.style.height = this.flowControlsList.canvas.style.height = yPosition + "px";
+            this._createCollapseAnimation(this.flowControlsList, this.flowControlsElement, flowControls, yPosition);
+        }
+
+        /**
+        * Clears the list of elements and removes the elements
+        */
+        public clearLists(): void {
+            for (var i = 0; i < this._listElements.length; i++) {
+                this._removeListElement(this._listElements[i]);
+            }
+            this._listElements.splice(0, this._listElements.length - 1);
+        }
+
+        /**
+        * Sets the color theme of the lists
+        * @param color: the theme color
+        */
+        public setColorTheme(color: string): void {
+            this.triggersList.canvas.style.backgroundColor = color;
+            this.actionsList.canvas.style.backgroundColor = color;
+            this.flowControlsList.canvas.style.backgroundColor = color;
+        }
+
+        /**
+        * Creates a list element
+        * @param paper: the Raphael.js paper
+        * @param yPosition: the y position of the element
+        * @param text: the element text
+        * @param type: the element type (trigger, action, flow control)
+        * @param textColor: the text color
+        * @param drag: if the element should be drag'n'dropped
+        */
+        private _createListElement(paper: Paper, yPosition: number, text: string, type: number,
+                                   textColor: RaphaelColor, drag: boolean, element?: Element): ListElement
+        {
+            var object = new ListElement();
+
+            object.rect = paper.rect(10, yPosition, 300, List.ELEMENT_HEIGHT);
+
+            object.text = paper.text(30, yPosition + object.rect.attr("height") / 2, text);
+            object.text.attr("fill", textColor);
+            object.text.attr("text-anchor", "start");
+            object.text.attr("font-size", "12");
+            object.text.attr("text-anchor", "start");
+            object.text.attr("font-family", "Sinkin Sans Light");
+
+            if (drag) {
+                this._createListElementAnimation(object);
+            }
+
+            object.type = type;
+            object.element = element;
+
+            this._listElements.push(object);
+            return object;
+        }
+
+        /**
+        * Removes a list element
+        * @param element: the element to remove
+        */
+        private _removeListElement(element: ListElement): void {
+            element.rect.remove();
+            element.text.remove();
+        }
+
+        /*
+        * Creates the collapse animation of a list
+        * @param paper: the list paper
+        * @param htmlElement: the list div container
+        * @param element: the list element to click on
+        * @param expandedHeight: the height when the list is expanded
+        */
+        private _createCollapseAnimation(paper: Paper, htmlElement: HTMLElement, element: ListElement, expandedHeight: number): void {
+            var onClick = (event: MouseEvent) => {
+                var height = htmlElement.style.height;
+                if (height === expandedHeight + "px") {
+                    htmlElement.style.height = paper.canvas.style.height = 35 + "px";
+                }
+                else {
+                    htmlElement.style.height = paper.canvas.style.height = expandedHeight + "px";
+                }
+            };
+
+            element.rect.click(onClick);
+        }
+
+        /*
+        * Creates the animation of a list element
+        * @param element: the list element to animate
+        */
+        private _createListElementAnimation(element: ListElement): void {
+            var onMove = (dx: number, dy: number, x: number, y: number) =>
+            { };
+
+            var onStart = (x: number, y: number, event: MouseEvent) => {
+                this._parentContainer.style.cursor = "copy";
+                element.rect.animate({
+                    x: -10,
+                    opacity: 0.25
+                }, 500, ">");
+                element.text.animate({
+                    x: 10,
+                    opacity: 0.25
+                }, 500, ">");
+            };
+
+            var onEnd = (event: MouseEvent) => {
+                this._parentContainer.style.cursor = "default";
+                element.rect.animate({
+                    x: 10,
+                    opacity: 1.0
+                }, 500, "<");
+                element.text.animate({
+                    x: 30,
+                    opacity: 1.0
+                }, 500, "<");
+
+                var dragResult = this._viewer.traverseGraph(null, this._viewer.mousex, this._viewer.mousey, false);
+
+                if (dragResult.hit) {
+                    if (element.type === Type.TRIGGER && dragResult.action !== this._viewer.root) {
+                        alert("Triggers can be dragged only on the root node (the mesh)");
+                        return;
+                    }
+
+                    if (element.type === Type.ACTION && dragResult.action === this._viewer.root) {
+                        alert("Please add a trigger before.");
+                        return;
+                    }
+
+                    if (element.type === Type.FLOW_CONTROL && (dragResult.action === this._viewer.root || (dragResult.action.type === Type.FLOW_CONTROL && dragResult.action.parent.hub === null))) {
+                        return;
+                    }
+
+                    if (element.type === Type.FLOW_CONTROL && dragResult.action.combineArray !== null) {
+                        alert("A condition cannot be handled by a Combine Action.");
+                        return;
+                    }
+
+                    if ((element.type === Type.FLOW_CONTROL || element.type === Type.ACTION) && dragResult.action.type === Type.TRIGGER && dragResult.action.children.length > 0) {
+                        alert("Triggers can have only one child. Please add another trigger of same type.");
+                        return;
+                    }
+
+                    if (!(dragResult.action.combineArray !== null) && dragResult.action.children.length > 0 && dragResult.action.type !== Type.TRIGGER && dragResult.action !== this._viewer.root) {
+                        alert("An action can have only one child.");
+                        return;
+                    }
+
+                    this._viewer.addAction(dragResult.action, element.type, element.element);
+                    this._viewer.update();
+                }
+            };
+
+            element.rect.drag(onMove, onStart, onEnd);
+            element.text.drag(onMove, onStart, onEnd);
+        }
+
+    }
+}

+ 105 - 0
Tools/ActionsBuilder/actionsbuilder.main.ts

@@ -0,0 +1,105 @@
+/*
+Global functions called by the plugins (3ds Max, etc.)
+*/
+
+// Elements
+
+var list: ActionsBuilder.List = null;
+var viewer: ActionsBuilder.Viewer = null;
+
+var actionsBuilderJsonInput: HTMLInputElement = <HTMLInputElement>document.getElementById("ActionsBuilderJSON");
+
+this.createJSON = () => {
+    var structure = viewer.utils.createJSON(viewer.root);
+    var asText = JSON.stringify(structure);
+    actionsBuilderJsonInput.value = asText;
+};
+
+this.loadFromJSON = () => {
+    var json = actionsBuilderJsonInput.value;
+    if (json !== "") {
+        var structure = JSON.parse(json);
+        viewer.utils.loadFromJSON(structure, null);
+    }
+};
+
+this.updateObjectName = () => {
+    var element = <HTMLInputElement>document.getElementById("ActionsBuilderObjectName");
+    var name = element.value;
+
+    viewer.objectName = name;
+
+    if (viewer.root.type === ActionsBuilder.Type.OBJECT) {
+        name += " - Mesh";
+    }
+    else {
+        name += " - Scene";
+    }
+
+    viewer.root.node.text.attr("text", name);
+};
+
+this.resetList = () => {
+    list.clearLists();
+    list.createListsElements();
+};
+
+this.setMeshesNames = (...args: string[]) => {
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.MESHES.push(args[i]);
+    }
+};
+
+this.setLightsNames = (...args: string[]) => {
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.LIGHTS.push(args[i]);
+    }
+};
+
+this.setCamerasNames = (...args: string[]) => {
+    for (var i = 0; i < args.length; i++) {
+        ActionsBuilder.SceneElements.CAMERAS.push(args[i]);
+    }
+};
+
+this.setSoundsNames = (...args: string[]) => {
+    for (var i = 0; i < args.length; i++) {
+        var sound = args[i];
+
+        if (sound !== "" && ActionsBuilder.SceneElements.SOUNDS.indexOf(sound) === -1) {
+            ActionsBuilder.SceneElements.SOUNDS.push(args[i]);
+        }
+    }
+};
+
+this.hideButtons = () => {
+    // Empty
+};
+
+this.setIsObject = () => {
+    viewer.root.type = ActionsBuilder.Type.OBJECT;
+};
+
+this.setIsScene = () => {
+    viewer.root.type = ActionsBuilder.Type.SCENE;
+};
+
+this.run = () => {
+    // Configure viewer
+    viewer = new ActionsBuilder.Viewer(ActionsBuilder.Type.OBJECT);
+
+    viewer.setColorTheme("-ms-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("-webkit-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+    viewer.setColorTheme("-o-linear-gradient(top, rgba(38, 38, 38,1) 0%, rgba(125, 126, 125, 1) 100%)");
+
+    viewer.onResize();
+    viewer.update();
+
+    // Configure list
+    list = new ActionsBuilder.List(viewer);
+    list.setColorTheme("rgb(64, 64, 64)");
+
+    list.createListsElements();
+    list.onResize();
+};

+ 551 - 0
Tools/ActionsBuilder/actionsbuilder.parameters.ts

@@ -0,0 +1,551 @@
+module ActionsBuilder {
+    export class Parameters {
+        public parametersContainer: HTMLElement;
+        public parametersHelpElement: HTMLElement;
+
+        private _action: Action = null;
+        private _viewer: Viewer;
+
+        /*
+        * Constructor
+        */
+        constructor(viewer: Viewer) {
+            // Get HTML elements
+            this.parametersContainer = document.getElementById("ParametersElementID");
+            this.parametersHelpElement = document.getElementById("ParametersHelpElementID");
+
+            // Configure this
+            this._viewer = viewer;
+
+            // Configure events
+            window.addEventListener("resize", (event: Event) => {
+                this.onResize(event);
+            });
+        }
+
+        /*
+        * Clears the parameters fileds in the parameters view
+        */
+        public clearParameters(): void {
+            if (this.parametersContainer.children === null) {
+                return;
+            }
+
+            while (this.parametersContainer.children.length > 0) {
+                this.parametersContainer.removeChild(this.parametersContainer.firstChild);
+            }
+        }
+
+        /*
+        * Creates parameters fields
+        * @param action: the action to configure
+        */
+        public createParameters(action: Action): void {
+            // Clear parameters fields and draw help description
+            this._action = action;
+            this.clearParameters();
+
+            if (action === null) {
+                return;
+            }
+
+            this._createHelpSection(action);
+            this._createNodeSection(action);
+
+            // Get properties
+            var properties = action.properties;
+            var propertiesResults = action.propertiesResults;
+
+            var targetParameterSelect: HTMLSelectElement = null;
+            var targetParameterNameSelect: HTMLSelectElement = null;
+            var propertyPathSelect: HTMLSelectElement = null;
+            var propertyPathOptionalSelect: HTMLSelectElement = null;
+
+            if (properties.length === 0) {
+                return;
+            }
+
+            // Draw properties
+            for (var i = 0; i < properties.length; i++) {
+                // Create separator
+                var separator = document.createElement("hr");
+                separator.noShade = true;
+                separator.className = "ParametersElementSeparatorClass";
+                this.parametersContainer.appendChild(separator);
+
+                // Create parameter text
+                var parameterName = document.createElement("a");
+                parameterName.text = properties[i].text;
+                parameterName.className = "ParametersElementTitleClass";
+                this.parametersContainer.appendChild(parameterName);
+
+                if (properties[i].text === "parameter" || properties[i].text === "target" || properties[i].text === "parent") {
+                    // Create target select element
+                    targetParameterSelect = document.createElement("select");
+                    targetParameterSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(targetParameterSelect);
+
+                    // Create target name select element
+                    targetParameterNameSelect = document.createElement("select");
+                    targetParameterNameSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(targetParameterNameSelect);
+
+                    // Events and configure
+                    (this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i))(null);
+                    targetParameterSelect.value = propertiesResults[i].targetType;
+                    targetParameterNameSelect.value = propertiesResults[i].value;
+
+                    targetParameterSelect.onchange = this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i);
+                    targetParameterNameSelect.onchange = this._parameterTargetNameChanged(targetParameterSelect, targetParameterNameSelect, i);
+                }
+                else if (properties[i].text === "propertyPath") {
+                    // Create property path select
+                    propertyPathSelect = document.createElement("select");
+                    propertyPathSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(propertyPathSelect);
+
+                    // Create additional select
+                    propertyPathOptionalSelect = document.createElement("select");
+                    propertyPathOptionalSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(propertyPathOptionalSelect);
+
+                    // Events and configure
+                    (this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect, i))(null);
+
+                    var property = this._action.propertiesResults[i].value.split(".");
+                    if (property.length > 0) {
+                        if (property.length === 1) {
+                            propertyPathSelect.value = property[0];
+                        }
+                        else {
+                            var completePropertyPath = "";
+                            for (var j = 0; j < property.length - 1; j++) {
+                                completePropertyPath += property[j];
+                                completePropertyPath += (j === property.length - 2) ? "" : ".";
+                            }
+                            propertyPathSelect.value = completePropertyPath;
+                            this._viewer.utils.setElementVisible(propertyPathOptionalSelect, true);
+                        }
+
+                        this._fillAdditionalPropertyPath(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect);
+                        propertyPathOptionalSelect.value = property[property.length - 1];
+
+                        if (propertyPathOptionalSelect.options.length === 0 || propertyPathOptionalSelect.options[0].text === "") {
+                            this._viewer.utils.setElementVisible(propertyPathOptionalSelect, false);
+                        }
+                    }
+
+                    targetParameterSelect.onchange = this._parameterTargetChanged(targetParameterSelect, targetParameterNameSelect, propertyPathSelect, propertyPathOptionalSelect, i - 1);
+                    propertyPathSelect.onchange = this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, propertyPathOptionalSelect, i);
+                    propertyPathOptionalSelect.onchange = this._additionalPropertyPathSelectChanged(propertyPathSelect, propertyPathOptionalSelect, i);
+                }
+                else if (properties[i].text === "operator") {
+                    var conditionOperatorSelect = document.createElement("select");
+                    conditionOperatorSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(conditionOperatorSelect);
+
+                    // Configure event
+                    (this._conditionOperatorSelectChanged(conditionOperatorSelect, i))(null);
+                    conditionOperatorSelect.value = propertiesResults[i].value;
+                    conditionOperatorSelect.onchange = this._conditionOperatorSelectChanged(conditionOperatorSelect, i);
+                }
+                else if (properties[i].text === "sound") {
+                    var soundSelect = document.createElement("select");
+                    soundSelect.className = "ParametersElementSelectClass";
+                    this.parametersContainer.appendChild(soundSelect);
+
+                    // Configure event
+                    (this._soundSelectChanged(soundSelect, i))(null);
+                    soundSelect.value = propertiesResults[i].value;
+                    soundSelect.onchange = this._soundSelectChanged(soundSelect, i);
+                }
+                else {
+                    if (propertiesResults[i].value === "true" || propertiesResults[i].value === "false") {
+                        var booleanSelect = document.createElement("select");
+                        booleanSelect.className = "ParametersElementSelectClass";
+                        this.parametersContainer.appendChild(booleanSelect);
+
+                        // Configure event
+                        (this._booleanSelectChanged(booleanSelect, i))(null);
+                        booleanSelect.value = propertiesResults[i].value;
+                        booleanSelect.onchange = this._booleanSelectChanged(booleanSelect, i);
+                    }
+                    else {
+                        var propertyInput = document.createElement("input");
+                        propertyInput.value = propertiesResults[i].value;
+                        propertyInput.className = "ParametersElementInputClass";
+                        this.parametersContainer.appendChild(propertyInput);
+
+                        // Configure event
+                        propertyInput.onkeyup = this._propertyInputChanged(propertyInput, i);
+                    }
+                }
+            }
+        }
+
+        /*
+        * Resizes the parameters view
+        * @param: the resize event
+        */
+        public onResize(event?: Event): void {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.parametersContainer.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 200 + "px";
+
+            this.parametersHelpElement.style.height = 200 + "px";
+        }
+
+        /*
+        * Returns the boolean select change event
+        * @param booleanSelect: the boolean select element
+        * @param indice: the properties result indice
+        */
+        private _booleanSelectChanged(booleanSelect: HTMLSelectElement, indice: number): (ev: Event) => void {
+            return (ev: Event) => {
+                if (booleanSelect.options.length === 0) {
+                    var values = ["true", "false"];
+                    for (var i = 0; i < values.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = values[i];
+                        booleanSelect.options.add(option);
+                    }
+                }
+                else {
+                    this._action.propertiesResults[indice].value = booleanSelect.value;
+                }
+            };
+        }
+
+        /*
+        * Returns the sound select change event
+        * @param soundSelect: the sound select element
+        * @param indice: the properties result indice
+        */
+        private _soundSelectChanged(soundSelect: HTMLSelectElement, indice: number): (ev: Event) => void {
+            return (ev: Event) => {
+                if (soundSelect.options.length === 0) {
+                    for (var i = 0; i < SceneElements.SOUNDS.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = SceneElements.SOUNDS[i];
+                        soundSelect.options.add(option);
+                    }
+
+                    this._sortList(soundSelect);
+                }
+                else {
+                    this._action.propertiesResults[indice].value = soundSelect.value;
+                }
+            };
+        }
+
+        /*
+        * Returns the condition opeator select changed event
+        * @param conditionOperatorSelect: the condition operator select element
+        * @param indice: the properties result indice
+        */
+        private _conditionOperatorSelectChanged(conditionOperatorSelect: HTMLSelectElement, indice: number): (ev: Event) => void {
+            return (ev: Event) => {
+                if (conditionOperatorSelect.options.length === 0) {
+                    for (var i = 0; i < SceneElements.OPERATORS.length; i++) {
+                        var option = document.createElement("option");
+                        option.value = option.text = SceneElements.OPERATORS[i];
+                        conditionOperatorSelect.options.add(option);
+                    }
+                }
+                else {
+                    this._action.propertiesResults[indice].value = conditionOperatorSelect.value;
+                }
+            };
+        }
+
+        /*
+        * Returns the property input changed event
+        * @param propertyInput: the property input
+        * @param indice: the properties result indice
+        */
+        private _propertyInputChanged(propertyInput: HTMLInputElement, indice: number): (ev: Event) => void {
+            return (ev: Event) => {
+                this._action.propertiesResults[indice].value = propertyInput.value;
+            };
+        }
+
+        /*
+        * Returns the propertyPath select changed event
+        * @param targetParameterSelect: the target/parameter select element
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        private _propertyPathSelectChanged(targetParameterSelect: HTMLSelectElement, propertyPathSelect: HTMLSelectElement,
+            additionalPropertyPathSelect: HTMLSelectElement, indice: number): (event: Event) => void
+        {
+            return (event: Event) => {
+                if (propertyPathSelect.options.length === 0) {
+                    // Configure start values
+                    var properties = this._getPropertiesFromType(targetParameterSelect.value);
+
+                    if (properties !== null) {
+                        for (var i = 0; i < properties.length; i++) {
+                            var option = document.createElement("option");
+                            option.value = option.text = properties[i];
+                            propertyPathSelect.options.add(option);
+                        }
+                    }
+                    
+                } else {
+                    // Set property
+                    this._action.propertiesResults[indice].value = propertyPathSelect.value;
+                }
+
+                // Configure addition property
+                this._fillAdditionalPropertyPath(targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect);
+
+                // Sort
+                this._sortList(propertyPathSelect);
+            };
+        }
+
+        private _fillAdditionalPropertyPath(targetParameterSelect: HTMLSelectElement, propertyPathSelect: HTMLSelectElement,
+            additionalPropertyPathSelect: HTMLSelectElement): void
+        {
+            additionalPropertyPathSelect.options.length = 0;
+            var object = this._getObjectFromType(targetParameterSelect.value);
+
+            if (object !== null) {
+                var propertyPath = propertyPathSelect.value.split(".");
+                for (var i = 0; i < propertyPath.length; i++) {
+                    object = object[propertyPath[i]];
+                }
+            }
+            if (object === null || object === undefined || (typeof (object)).toLowerCase() === "string") {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, false);
+                return;
+            }
+
+            // Add options
+            var emptyOption = document.createElement("option");
+            emptyOption.value = emptyOption.text = "";
+            additionalPropertyPathSelect.add(emptyOption);
+
+            for (var thing in object) {
+                var type = SceneElements.GetInstanceOf(object[thing]);
+                var index = SceneElements.TYPES.indexOf(type);
+
+                if (index !== -1) {
+                    var option = document.createElement("option");
+                    option.value = option.text = thing;
+                    additionalPropertyPathSelect.options.add(option);
+                    emptyOption.text += thing + ", ";
+                }
+            }
+
+            if (additionalPropertyPathSelect.options.length === 0 || additionalPropertyPathSelect.options[0].text === "") {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, false);
+            }
+            else {
+                this._viewer.utils.setElementVisible(additionalPropertyPathSelect, true);
+            }
+        }
+
+        /*
+        * Returns the additional propertyPath select changed event
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        private _additionalPropertyPathSelectChanged(propertyPathSelect: HTMLSelectElement, additionalPropertyPathSelect: HTMLSelectElement,
+            indice: number): (event: Event) => void
+        {
+            return (event: Event) => {
+                var property = propertyPathSelect.value;
+                var additionalProperty = additionalPropertyPathSelect.value;
+
+                if (additionalProperty !== "") {
+                    property += ".";
+                    property += additionalPropertyPathSelect.value;
+                }
+                this._action.propertiesResults[indice].value = property;
+            };
+        }
+
+        /*
+        * Returns the parameter/target select changed event
+        * @param targetParameterSelect: the target/parameter select element
+        * @param targetParameterNameSelect: the target/parameter name select element
+        * @param propertyPathSelect: the propertyPath select element
+        * @param additionalPropertyPathSelect: the additional propertyPath select element
+        * @param indice: the properties indice in action.properties
+        */
+        private _parameterTargetChanged(targetParameterSelect: HTMLSelectElement, targetParameterNameSelect: HTMLSelectElement,
+            propertyPathSelect: HTMLSelectElement, additionalPropertyPathSelect: HTMLSelectElement, indice: number): (event: Event) => void
+        {
+            return (event: Event) => {
+                if (targetParameterSelect.options.length === 0) {
+                    // Configure start values
+                    var options = [
+                        { text: "Mesh", targetType: "MeshProperties" },
+                        { text: "Light", targetType: "LightProperties" },
+                        { text: "Camera", targetType: "CameraProperties" },
+                        { text: "Scene", targetType: "SceneProperties" }
+                    ];
+                    targetParameterSelect.options.length = 0;
+
+                    for (var i = 0; i < options.length; i++) {
+                        var option = document.createElement("option");
+                        option.text = options[i].text;
+                        option.value = options[i].targetType;
+                        targetParameterSelect.options.add(option);
+                    }
+                } else {
+                    this._action.propertiesResults[indice].targetType = targetParameterSelect.value;
+                    this._action.propertiesResults[indice].value = "";
+
+                    if (propertyPathSelect !== null) {
+                        this._action.propertiesResults[indice + 1].value = ""; // propertyPath
+                    }
+                }
+
+                // Configure target names
+                var targetParameterProperties = this._getTargetFromType(targetParameterSelect.value);
+                targetParameterNameSelect.options.length = 0;
+
+                if (targetParameterProperties !== null) {
+                    for (var i = 0; i < targetParameterProperties.length; i++) {
+                        var option = document.createElement("option");
+                        option.text = option.value = targetParameterProperties[i];
+                        targetParameterNameSelect.options.add(option);
+                    }
+                }
+
+                // Clear property path
+                if (propertyPathSelect !== null) {
+                    propertyPathSelect.options.length = 0;
+                    additionalPropertyPathSelect.options.length = 0;
+                    this._propertyPathSelectChanged(targetParameterSelect, propertyPathSelect, additionalPropertyPathSelect, indice + 1)(null);
+                }
+
+                this._sortList(targetParameterNameSelect);
+                this._sortList(targetParameterSelect);
+            };
+        }
+
+        /*
+        * Returns the parameter/target name select changed
+        * @param indice: the properties indice to change
+        */
+        private _parameterTargetNameChanged(targetParameterSelect: HTMLSelectElement, targetParameterNameSelect: HTMLSelectElement, indice: number): (event: Event) => void {
+            return (event: Event) => {
+                this._action.propertiesResults[indice].value = targetParameterNameSelect.value;
+            };
+        }
+
+        /*
+        * Returns the array of objects names in function of its type
+        * @param type: the target type
+        */
+        public _getTargetFromType(type: string): Array<string> {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return SceneElements.MESHES;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return SceneElements.LIGHTS;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return SceneElements.CAMERAS;
+            }
+
+            return null;
+        }
+
+        /*
+        * Returns the properties in function of its type
+        * @param type: the target type
+        */
+        private _getPropertiesFromType(type: string): Array<string> {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return SceneElements.MESH_PROPERTIES;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return SceneElements.LIGHT_PROPERTIES;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return SceneElements.CAMERA_PROPERTIES;
+            }
+            if (type === "SceneProperties" || type === "Scene") {
+                return SceneElements.SCENE_PROPERTIES;
+            }
+
+            return null;
+        }
+
+        /*
+        * Returns the object in function of the given type
+        * @param type: the target type
+        */
+        public _getObjectFromType(type: string): any {
+            if (type === "MeshProperties" || type === "Mesh") {
+                return SceneElements.MESH;
+            }
+            if (type === "LightProperties" || type === "Light") {
+                return SceneElements.LIGHT;
+            }
+            if (type === "CameraProperties" || type === "Camera") {
+                return SceneElements.CAMERA;
+            }
+            if (type === "SceneProperties" || type === "Scene") {
+                return SceneElements.SCENE;
+            }
+
+            return null;
+        }
+
+        /*
+        * Creates the node section (top of parameters)
+        * @param action: the action element to get color, text, name etc.
+        */
+        private _createNodeSection(action: Action): void {
+            var element = document.createElement("div");
+            element.style.background = <string>this._viewer.getSelectedNodeColor(action.type, action.node.detached);
+            element.className = "ParametersElementNodeClass";
+
+            var text = document.createElement("a");
+            text.text = action.name;
+            text.className = "ParametersElementNodeTextClass";
+
+            element.appendChild(text);
+            this.parametersContainer.appendChild(element);
+        }
+
+        /*
+        * Creates the help section
+        * @param action : the action containing the description
+        */
+        private _createHelpSection(action: Action): void {
+            // Get description
+            var element = Elements.GetElementFromName(action.name);
+
+            if (element !== null) {
+                this.parametersHelpElement.textContent = element.description;
+            }
+        }
+
+        /*
+        * Alphabetically sorts a HTML select element options
+        * @param element : the HTML select element to sort
+        */
+        private _sortList(element: HTMLSelectElement): void {
+            var options = [];
+
+            for (var i = element.options.length - 1; i >= 0; i--) {
+                options.push(element.removeChild(element.options[i]));
+            }
+
+            options.sort((a, b) => {
+                return a.innerHTML.localeCompare(b.innerHTML);
+            });
+
+            for (var i = 0; i < options.length; i++) {
+                element.options.add(options[i]);
+            }
+        }
+    }
+} 

+ 94 - 0
Tools/ActionsBuilder/actionsbuilder.toolbar.ts

@@ -0,0 +1,94 @@
+module ActionsBuilder {
+    export class Toolbar {
+        public toolbarElement: HTMLElement;
+        public saveActionGraphElement: HTMLElement;
+
+        private _viewer: Viewer;
+
+        constructor(viewer: Viewer) {
+            // Get HTML elements
+            this.toolbarElement = document.getElementById("ToolbarElementID");
+
+            // Configure this
+            this._viewer = viewer;
+
+            // Manage events
+            window.addEventListener("resize", (event: Event) => {
+                this.onResize();
+            });
+
+            // Bottom toolbar
+            document.getElementById("ViewerDeZoomID").addEventListener("click", (event: MouseEvent) => {
+                if (this._viewer.zoom > 0.1) {
+                    this._viewer.zoom -= 0.1;
+                }
+                this._viewer.update();
+            });
+            document.getElementById("ViewerZoomID").addEventListener("click", (event: MouseEvent) => {
+                if (this._viewer.zoom < 1.0) {
+                    this._viewer.zoom += 0.1;
+                }
+                this._viewer.update();
+            });
+            document.getElementById("ViewerReconnectAll").addEventListener("click", (event: MouseEvent) => {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.selectedNode = this._viewer.root.children[i];
+                    this._viewer.utils.onDetachAction(false, true);
+                }
+                this._viewer.update();
+                this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerDisconnectAll").addEventListener("click", (event: MouseEvent) => {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.selectedNode = this._viewer.root.children[i];
+                    this._viewer.utils.onDetachAction(true, false);
+                }
+                this._viewer.update();
+                this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerReduceAll").addEventListener("click", (event: MouseEvent) => {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.selectedNode = this._viewer.root.children[i];
+                    this._viewer.utils.onReduceAll(false);
+                }
+                this._viewer.update();
+                this._viewer.selectedNode = null;
+            });
+            document.getElementById("ViewerExpandAll").addEventListener("click", (event: MouseEvent) => {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.selectedNode = this._viewer.root.children[i];
+                    this._viewer.utils.onReduceAll(true);
+                }
+                this._viewer.update();
+                this._viewer.selectedNode = null;
+            });
+
+            // Top toolbar
+            this.saveActionGraphElement = document.getElementById("ToolsButtonIDSaveActionGraph");
+            this.drawSaveActionGraphButton(false);
+
+            document.getElementById("ResetActionGraphID").addEventListener("click", (event: MouseEvent) => {
+                if (confirm("Are you sure?")) {
+                    for (var i = 0; i < this._viewer.root.children.length; i++) {
+                        this._viewer.selectedNode = this._viewer.root.children[i];
+                        this._viewer.utils.onRemoveBranch();
+                    }
+                    this._viewer.update();
+                    this._viewer.selectedNode = null;
+                }
+            });
+            document.getElementById("TestActionGraphID").addEventListener("click", (event: MouseEvent) => {
+                this._viewer.utils.onTestGraph();
+            });
+        }
+
+        public onResize(): void {
+            this.toolbarElement.style.top = this._viewer.viewerElement.clientHeight + 20 + "px";
+        }
+
+        public drawSaveActionGraphButton(draw: boolean) {
+            this.saveActionGraphElement.style.display = draw ? "block" : "none";
+        }
+    }
+}
+ 

+ 326 - 0
Tools/ActionsBuilder/actionsbuilder.ts

@@ -0,0 +1,326 @@
+module ActionsBuilder {
+
+    /**
+    * Defines static types
+    */
+    export class Type {
+        private static _TRIGGER = 0;
+        private static _ACTION = 1;
+        private static _FLOW_CONTROL = 2;
+        private static _OBJECT = 3;
+        private static _SCENE = 4;
+
+        public static get TRIGGER(): number {
+            return Type._TRIGGER;
+        }
+
+        public static get ACTION(): number {
+            return Type._ACTION;
+        }
+
+        public static get FLOW_CONTROL(): number {
+            return Type._FLOW_CONTROL;
+        }
+
+        public static get OBJECT(): number {
+            return Type._OBJECT;
+        }
+
+        public static get SCENE(): number {
+            return Type._SCENE;
+        }
+    }
+
+    /*
+    * Defines the BABYLON.JS elements
+    */
+    export class SceneElements {
+        /*
+        * BabylonJS objects
+        */
+        private static _ENGINE: BABYLON.Engine = new BABYLON.Engine(<HTMLCanvasElement>document.getElementById("RenderCanvasID"));
+        private static _SCENE: BABYLON.Scene = new BABYLON.Scene(SceneElements.ENGINE);
+        private static _MESH: BABYLON.Mesh = new BABYLON.Mesh("mesh", SceneElements._SCENE);
+        private static _LIGHT: BABYLON.Light = new BABYLON.Light("light", SceneElements._SCENE);
+        private static _CAMERA: BABYLON.Camera = new BABYLON.Camera("camera", BABYLON.Vector3.Zero(), SceneElements._SCENE);
+
+        public static get ENGINE(): BABYLON.Engine {
+            return SceneElements._ENGINE;
+        }
+
+        public static get SCENE(): BABYLON.Scene {
+            return SceneElements._SCENE;
+        }
+
+        public static get MESH(): BABYLON.Mesh {
+            return SceneElements._MESH;
+        }
+
+        public static get LIGHT(): BABYLON.Light {
+            return SceneElements._LIGHT;
+        }
+
+        public static get CAMERA(): BABYLON.Camera {
+            return SceneElements._CAMERA;
+        }
+
+        /*
+        * Objects names
+        */
+        private static _MESHES = new Array<string>();
+        private static _LIGHTS = new Array<string>();
+        private static _CAMERAS = new Array<string>();
+        private static _SOUNDS = new Array<string>();
+
+        public static get MESHES(): Array<string> {
+            return SceneElements._MESHES;
+        }
+
+        public static get LIGHTS(): Array<string> {
+            return SceneElements._LIGHTS;
+        }
+
+        public static get CAMERAS(): Array<string> {
+            return SceneElements._CAMERAS;
+        }
+
+        public static get SOUNDS(): Array<string> {
+            return SceneElements._SOUNDS;
+        }
+
+        /*
+        * Properties
+        */
+        private static _MESH_PROPERTIES = new Array<string>();
+        private static _LIGHT_PROPERTIES = new Array<string>();
+        private static _CAMERA_PROPERTIES = new Array<string>();
+        private static _SCENE_PROPERTIES = new Array<string>();
+
+        public static get MESH_PROPERTIES(): Array<string> {
+            return SceneElements._MESH_PROPERTIES;
+        }
+
+        public static get LIGHT_PROPERTIES(): Array<string> {
+            return SceneElements._LIGHT_PROPERTIES;
+        }
+
+        public static get CAMERA_PROPERTIES(): Array<string> {
+            return SceneElements._CAMERA_PROPERTIES;
+        }
+
+        public static get SCENE_PROPERTIES(): Array<string> {
+            return SceneElements._SCENE_PROPERTIES;
+        }
+
+        /*
+        * Types
+        */
+        private static _TYPES = new Array<string>();
+
+        public static get TYPES(): Array<string> {
+            return SceneElements._TYPES;
+        }
+
+        /*
+        * Operators
+        */
+        private static _OPERATORS = new Array<string>();
+
+        public static get OPERATORS(): Array<string> {
+            return SceneElements._OPERATORS;
+        }
+
+        /*
+        * Methods
+        */
+        public static GetInstanceOf(object: Object): string {
+            if (object === null || object === undefined) {
+                return "";
+            }
+            return object.constructor.toString().match(/function (\w*)/)[1];
+        }
+
+        public static TestInstanceOf (object: Object, propertyName: string): boolean {
+            if (object === null || object.constructor === null) {
+                return false;
+            }
+
+            if (propertyName.length > 0 && propertyName[0] === "_")
+                return false;
+
+            var name = SceneElements.GetInstanceOf(object);
+
+            for (var i = 0; i < SceneElements.TYPES.length; i++) {
+                if (name === SceneElements.TYPES[i]) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+
+    // Functions
+    var specialTypes = [
+        "StandardMaterial"
+    ];
+    SceneElements.MESH.material = new BABYLON.StandardMaterial("material", SceneElements.SCENE);
+
+    var addSpecialType = (object: any, properties: Array<string>, thing: string) => {
+        for (var specialThing in object[thing]) {
+            if (object[thing].hasOwnProperty(specialThing) && SceneElements.TestInstanceOf(object[thing][specialThing], specialThing)) {
+                properties.push(thing + "." + specialThing);
+            }
+        }
+    };
+
+    // Configure types
+    SceneElements.TYPES.push("Color3");
+    SceneElements.TYPES.push("Boolean");
+    SceneElements.TYPES.push("Number");
+    SceneElements.TYPES.push("Vector2");
+    SceneElements.TYPES.push("Vector3");
+    SceneElements.TYPES.push("String");
+
+    // Configure operators
+    SceneElements.OPERATORS.push("IsEqual");
+    SceneElements.OPERATORS.push("IsDifferent");
+    SceneElements.OPERATORS.push("IsGreater");
+    SceneElements.OPERATORS.push("IsLesser");
+
+    // Configure properties
+    for (var thing in SceneElements.MESH) {
+        var instance = SceneElements.GetInstanceOf(SceneElements.MESH[thing]);
+
+        if (SceneElements.MESH.hasOwnProperty(thing)) {
+            if (specialTypes.indexOf(instance) !== -1) {
+                addSpecialType(SceneElements.MESH, SceneElements.MESH_PROPERTIES, thing);
+            }
+            else if (SceneElements.TestInstanceOf(SceneElements.MESH[thing], thing)) {
+                SceneElements.MESH_PROPERTIES.push(thing);
+            }
+        }
+    }
+
+    for (var thing in SceneElements.LIGHT) {
+        if (SceneElements.LIGHT.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.LIGHT[thing], thing)) {
+            SceneElements.LIGHT_PROPERTIES.push(thing);
+        }
+    }
+
+    for (var thing in SceneElements.CAMERA) {
+        if (SceneElements.CAMERA.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.CAMERA[thing], thing)) {
+            SceneElements.CAMERA_PROPERTIES.push(thing);
+        }
+    }
+
+    for (var thing in SceneElements.SCENE) {
+        if (SceneElements.SCENE.hasOwnProperty(thing) && SceneElements.TestInstanceOf(SceneElements.SCENE[thing], thing)) {
+            SceneElements.SCENE_PROPERTIES.push(thing);
+        }
+    }
+
+    /**
+    * Defines an element property
+    */
+    export interface ElementProperty {
+        targetType?: string;
+        text: string;
+        value: string;
+    }
+
+    /**
+    * Defines an element property result
+    */
+    export interface ElementPropertyResult {
+        targetType?: string;
+        value: string;
+    }
+
+    /**
+    * Generic element, has a name, a text to draw, a description
+    * and a list of properties (ElementProperty)
+    */
+    export interface Element {
+        name: string;
+        text: string;
+        properties: Array<ElementProperty>;
+        description: string;
+    }
+
+    /**
+    * Actions Builder elements (triggers, actions & flow controls) that are
+    * arrays of Element
+    */
+    export class Elements {
+        private static _TRIGGERS = new Array<Element>();
+        private static _ACTIONS = new Array<Element>();
+        private static _FLOW_CONTROLS = new Array<Element>();
+
+        public static get TRIGGERS(): Array<Element> {
+            return Elements._TRIGGERS;
+        }
+
+        public static get ACTIONS(): Array<Element> {
+            return Elements._ACTIONS;
+        }
+
+        public static get FLOW_CONTROLS(): Array<Element> {
+            return Elements._FLOW_CONTROLS;
+        }
+
+        public static GetElementFromName(name: string): Element {
+            for (var i = 0; i < Elements.TRIGGERS.length; i++) {
+                if (Elements.TRIGGERS[i].name === name) {
+                    return Elements._TRIGGERS[i];
+                }
+            }
+
+            for (var i = 0; i < Elements.ACTIONS.length; i++) {
+                if (Elements.ACTIONS[i].name === name) {
+                    return Elements._ACTIONS[i];
+                }
+            }
+
+            for (var i = 0; i < Elements.FLOW_CONTROLS.length; i++) {
+                if (Elements.FLOW_CONTROLS[i].name === name) {
+                    return Elements._FLOW_CONTROLS[i];
+                }
+            }
+
+            return null;
+        }
+    }
+
+    // Configure triggers
+    Elements.TRIGGERS.push({ name: "OnPickTrigger", text: "pick", properties: [], description: "When the user picks the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnLeftPickTrigger", text: "left pick", properties: [], description: "When the user picks the edited mesh using the left click" });
+    Elements.TRIGGERS.push({ name: "OnRightPickTrigger", text: "right pick", properties: [], description: "When the user picks the edited mesh using the right click" });
+    Elements.TRIGGERS.push({ name: "OnCenterPickTrigger", text: "center pick", properties: [], description: "When the user picks the edited mesh using the click of the mouse wheel" });
+    Elements.TRIGGERS.push({ name: "OnPointerOverTrigger", text: "pointer over", properties: [], description: "When the user's mouse is over the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnPointerOutTrigger", text: "pointer out", properties: [], description: "When the user's mouse is out of the edited mesh" });
+    Elements.TRIGGERS.push({ name: "OnEveryFrameTrigger", text: "every frame", properties: [], description: "This trigger is called each frame (only on scene)" });
+    Elements.TRIGGERS.push({ name: "OnIntersectionEnterTrigger", text: "intersection enter", properties: [{ targetType: "MeshProperties", text: "parameter", value: "Object name?" }], description: "When the edited mesh intersects the another mesh predefined in the options" });
+    Elements.TRIGGERS.push({ name: "OnIntersectionExitTrigger", text: "intersection exit", properties: [{ targetType: "MeshProperties", text: "parameter", value: "Object name?" }], description: "When the edited mesh exits intersection with the another mesh predefined in the options" });
+    Elements.TRIGGERS.push({ name: "OnKeyDownTrigger", text: "key down", properties: [{ targetType: null, text: "parameter:", value: "a" }], description: "When the user pressed a key (enter the key character, example: \"r\")" });
+    Elements.TRIGGERS.push({ name: "OnKeyUpTrigger", text: "key up", properties: [{ targetType: null, text: "parameter:", value: "a" }], description: "When the user unpressed a key (enter the key character, example: \"p\")" });
+
+    // Configure actions
+    Elements.ACTIONS.push({ name: "SwitchBooleanAction", text: "switch boolean", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }], description: "Switches the boolean value of a given parameter of the target object: true to false, or false to true" });
+    Elements.ACTIONS.push({ name: "SetStateAction", text: "set state", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "value", value: "" }], description: "Sets a new state value for the target object (example: \"off\" or \"on\")" });
+    Elements.ACTIONS.push({ name: "SetValueAction", text: "set value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }], description: "Sets a new value to the specified parameter of the target object (example: position.x to 0.0)" });
+    Elements.ACTIONS.push({ name: "SetParentAction", text: "set parent", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "parent", value: "" }], description: "Sets the new parent of the target object (example: a mesh or a light)" });
+    Elements.ACTIONS.push({ name: "IncrementValueAction", text: "increment value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }], description: "Increments the value of the given parameter of the target object. The value can be negative. (example: increment position.x of 5.0)" });
+    Elements.ACTIONS.push({ name: "PlayAnimationAction", text: "play animation", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "from", value: "0" }, { text: "to", value: "150" }, { text: "loop", value: "false" }], description: "Plays an animation of the target object. Specify the start frame, the end frame and if the animation should loop." });
+    Elements.ACTIONS.push({ name: "StopAnimationAction", text: "stop animation", properties: [{ targetType: "MeshProperties", text: "target", value: "" }], description: "Stops the animations of the target object." });
+    Elements.ACTIONS.push({ name: "DoNothingAction", text: "do nothing", properties: [], description: "Does nothing, can be used to balance/equilibrate the actions graph." });
+    Elements.ACTIONS.push({ name: "InterpolateValueAction", text: "interpolate value", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "0" }, { text: "duration", value: "1000" }, { text: "stopOtherAnimations", value: "false" }], description: "Creates an animation (key frames) that animates the target object by interpolating the given parameter of the target value." });
+    Elements.ACTIONS.push({ name: "PlaySoundAction", text: "play sound", properties: [{ text: "sound", value: "" }], description: "Plays the specified sound." });
+    Elements.ACTIONS.push({ name: "StopSoundAction", text: "stop sound", properties: [{ text: "sound", value: "" }], description: "Stops the specified sound." });
+    Elements.ACTIONS.push({ name: "CombineAction", text: "combine", properties: [], description: "Special action that combines multiple actions. The combined actions are executed at the same time. Drag'n'drop the new actions inside to combine actions." });
+
+    // Configure flow control
+    Elements.FLOW_CONTROLS.push({ name: "ValueCondition", text: "value condition", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "propertyPath", value: "" }, { text: "value", value: "" }, { text: "operator", value: SceneElements.OPERATORS[0] }], description: "A condition checking if a given value is equal, different, lesser or greater than the given parameter of the target object" });
+    Elements.FLOW_CONTROLS.push({ name: "StateCondition", text: "state condition", properties: [{ targetType: "MeshProperties", text: "target", value: "" }, { text: "value", value: "" }], description: "A condition checking if the target object's state is equal to the given state. See \"set state\" action to set a state to an object." });
+    Elements.FLOW_CONTROLS.push({ name: "Hub", text: "hub", properties: [], description: "The hub is internally used by the Combine Action. It allows to add children to the Combine Action" });
+}

+ 527 - 0
Tools/ActionsBuilder/actionsbuilder.utils.ts

@@ -0,0 +1,527 @@
+interface Window {
+    clipboardData: any;
+}
+
+module ActionsBuilder {
+    export interface JSONObjectProperty {
+        targetType?: string;
+        name: string;
+        value: string;
+    }
+
+    export interface JSONObject {
+        type: number;
+        name: string;
+        detached: boolean;
+        children: Array<JSONObject>;
+        combine: Array<JSONObject>;
+        properties: Array<JSONObjectProperty>;
+    }
+
+    export class Utils {
+        // Members
+        public copiedStructure: string = null;
+
+        private _viewer: Viewer;
+
+        /*
+        * Constructor
+        * @param viewer: the viewer instance
+        */
+        constructor(viewer: Viewer) {
+            // Configure this
+            this._viewer = viewer;
+        }
+
+        /*
+        * Tests the graph and reports errors
+        */
+        public onTestGraph(): void {
+            if (this._viewer.root.children.length === 0) {
+                alert("Please add at least a Trigger and an Action to test the graph");
+            }
+
+            var onTestTarget = (targetType: string, target: any) => {
+                var targetExists = false;
+                var array = this._viewer.parameters._getTargetFromType(targetType);
+
+                if (array === null) {
+                    return targetExists;
+                }
+
+                for (var i = 0; i < array.length; i++) {
+                    if (array[i] === target) {
+                        targetExists = true;
+                        break;
+                    }
+                }
+
+                return targetExists;
+            };
+
+            var onNodeError = (action: Action): boolean => {
+                var node = action.node;
+                node.rect.attr("fill", Raphael.rgb(255, 0, 0));
+
+                return false;
+            };
+
+            var onTestAction = (action: Action): boolean => {
+                console.log("Testing " + action.name);
+
+                if (action.combineArray !== null) {
+                    var foundError = false;
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        if (!onTestAction(action.combineArray[i])) {
+                            foundError = true;
+                        }
+                    }
+
+                    if (foundError) {
+                        return false;
+                    }
+                }
+                else {
+                    // Test properties
+                    var properties = action.properties;
+                    var propertiesResults = action.propertiesResults;
+
+                    if (properties !== null) {
+                        var object: any = null;
+                        var propertyPath: any = null;
+
+                        for (var i = 0; i < properties.length; i++) {
+                            // Target
+                            if (properties[i].text === "target" || properties[i].text === "parent") {
+                                object = this._viewer.parameters._getObjectFromType(properties[i].targetType);
+                                var targetExists = onTestTarget(propertiesResults[i].targetType, propertiesResults[i].value);
+
+                                if (!targetExists) {
+                                    return onNodeError(action);
+                                }
+                            }
+                            // Property Path
+                            else if (properties[i].text === "propertyPath") {
+                                var property = <string>propertiesResults[i].value;
+                                var effectiveProperty = object;
+
+                                var p = property.split(".");
+                                for (var j = 0; j < p.length && effectiveProperty !== undefined; j++) {
+                                    effectiveProperty = effectiveProperty[p[j]];
+                                }
+
+                                if (effectiveProperty === undefined) {
+                                    return onNodeError(action);
+                                }
+                                else {
+                                    propertyPath = effectiveProperty;
+                                }
+                            }
+                            // value
+                            else if (properties[i].text == "value" && propertyPath != null) {
+                                var value = propertiesResults[i].value;
+
+                                if (!isNaN(propertyPath)) {
+                                    var num = parseFloat(value);
+                                    if (isNaN(num) || value === "") {
+                                        return onNodeError(action);
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    var foundError = false;
+                    for (var i = 0; i < action.children.length; i++) {
+                        if (!onTestAction(action.children[i])) {
+                            foundError = true;
+                        }
+                    }
+
+                    return !foundError;
+                }
+            };
+
+            var root = this._viewer.root;
+            var foundError = false;
+
+            for (var i = 0; i < root.children.length; i++) {
+                var trigger = root.children[i];
+                var properties = trigger.properties;
+
+                // Test properties of trigger (parameter)
+                if (properties !== null && properties.length > 0) {
+                    // Only one property
+                    var parameter = trigger.propertiesResults[0].value;
+
+                    if (properties[0].targetType !== null) {
+                        // Intersection trigger
+                        if (!onTestTarget("MeshProperties", parameter)) {
+                            foundError = onNodeError(trigger);
+                        }
+                    }
+                    else {
+                        // Key trigger
+                        if (!parameter.match(/[a-z]/)) {
+                            foundError = onNodeError(trigger);
+                        }
+                    }
+                }
+
+                for (var j = 0; j < trigger.children.length; j++) {
+                    var child = trigger.children[j];
+                    var result = onTestAction(child);
+
+                    if (!result) {
+                        foundError = true;
+                    }
+                }
+            }
+
+            if (foundError) {
+                alert("Found error(s). the red nodes contain the error.");
+            }
+            else {
+                alert("No error found.");
+            }
+        }
+
+        /*
+        * Recursively reduce/expand nodes
+        */
+        public onReduceAll(forceExpand: boolean = false): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var action = this._viewer.selectedNode;
+
+            if (action.combineArray !== null) {
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    this._viewer.selectedNode = action.combineArray[i];
+                    this.onReduce(forceExpand, !forceExpand);
+                }
+            }
+            else {
+                this.onReduce(forceExpand, !forceExpand);
+            }
+
+            for (var i = 0; i < action.children.length; i++) {
+                this._viewer.selectedNode = action.children[i];
+                this.onReduceAll(forceExpand);
+            }
+        }
+
+        /*
+        * Reduces the selected node
+        */
+        public onReduce(forceExpand: boolean = false, forceReduce: boolean = false): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var node = this._viewer.selectedNode.node;
+            node.rect.stop(node.rect.animation);
+
+            // Set minimized
+            if (forceExpand === true) {
+                node.minimized = false;
+            }
+            else if (forceReduce === true) {
+                node.minimized = true;
+            }
+            else {
+                node.minimized = !node.minimized;
+            }
+
+            // Set size
+            if (node.minimized) {
+                node.text.hide();
+                node.rect.attr("width", Viewer.NODE_MINIMIZED_WIDTH * this._viewer.zoom);
+            }
+            else {
+                node.text.show();
+                node.rect.attr("width", Viewer.NODE_WIDTH * this._viewer.zoom);
+            }
+        }
+
+        /*
+        * Detaches the selected action
+        */
+        public onDetachAction(forceDetach: boolean = false, forceAttach: boolean = false): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var action = this._viewer.selectedNode;
+
+            if (forceDetach === true) {
+                action.node.detached = true;
+            }
+            else if (forceAttach === true) {
+                action.node.detached = false;
+            }
+            else {
+                action.node.detached = !action.node.detached;
+            }
+
+            var onSetColor = (root: Action, detached: boolean) => {
+                var rootNode = root.node;
+                rootNode.rect.attr("fill", this._viewer.getNodeColor(root.type, detached));
+
+                if (root.combineArray !== null) {
+                    for (var i = 0; i < root.combineArray.length; i++) {
+                        var combineNode = root.combineArray[i].node;
+                        combineNode.rect.attr("fill", this._viewer.getNodeColor(root.combineArray[i].type, detached));
+                    }
+                }
+
+                for (var i = 0; i < root.children.length; i++) {
+                    onSetColor(root.children[i], detached);
+                }
+            };
+
+            onSetColor(action, action.node.detached);
+        }
+
+        /*
+        * Removes the selected node
+        */
+        public onRemoveNode(): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var action = this._viewer.selectedNode;
+            var parent = action.parent;
+
+            // If trigger, remove branch
+            if (action.type === Type.TRIGGER) {
+                this.onRemoveBranch();
+                return;
+            }
+
+            // If it is a combine hub
+            if (action.type === Type.FLOW_CONTROL && parent !== null && parent.combineArray !== null) {
+                action = parent;
+                parent = action.parent;
+            }
+
+            // Remove
+            if (parent !== null && parent.combineArray !== null) {
+                parent.removeCombinedAction(action);
+                if (parent.combineArray.length === 0) {
+                    parent.node.text.attr("text", "combine");
+
+                }
+            }
+            else {
+                if (action.combineArray !== null) {
+                    action.removeChild(action.hub);
+                }
+                action.parent.removeChild(action);
+            }
+
+            if (action.combineArray !== null) {
+                this._viewer.removeAction(action.hub, false);
+            }
+            this._viewer.removeAction(action, false);
+
+            // Finish
+            this._viewer.update();
+            this._viewer.parameters.clearParameters();
+            this._viewer.selectedNode = null;
+        }
+
+        /*
+        * Removes a branch starting from the selected node
+        */
+        public onRemoveBranch(): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            if (this._viewer.selectedNode === this._viewer.root) {
+                alert("Cannot remove the root node");
+                return;
+            }
+
+            var action = this._viewer.selectedNode;
+            var parent = action.parent;
+
+            // If combine
+            if (action.parent !== null && action.parent.combineArray !== null) {
+                action = parent;
+                parent = action.parent;
+            }
+
+            // Remove
+            if (action.combineArray !== null) {
+                action.removeChild(action.hub);
+            }
+            action.parent.removeChild(action);
+            this._viewer.removeAction(action, true);
+
+            // Finish
+            this._viewer.update();
+            this._viewer.parameters.clearParameters();
+            this._viewer.selectedNode = null;
+        }
+
+        /*
+        * Copies the selected structure
+        */ 
+        public onCopyStructure(): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var structure = this.createJSON(this._viewer.selectedNode);
+            var asText = JSON.stringify(structure);
+
+            if (window.clipboardData !== undefined) {
+                window.clipboardData.setData("text", asText);
+            }
+            else {
+                this.copiedStructure = asText;
+            }
+        }
+
+        /*
+        * Pastes the graph structure previously copied
+        */
+        public onPasteStructure(): void {
+            if (this._viewer.selectedNode === null) {
+                return;
+            }
+
+            var asText = (window.clipboardData !== undefined) ? window.clipboardData.getData("text") : this.copiedStructure;
+            var isJson = asText.length > 0 && asText[0] == "{" && asText[asText.length - 1] == "}";
+            var structure: JSONObject = JSON.parse(asText);
+            var action = this._viewer.selectedNode;
+
+            if (structure.type === Type.TRIGGER && action !== this._viewer.root) {
+                alert("You can't paste a trigger if the selected node isn't the root object");
+                return;
+            }
+
+            if (structure.type !== Type.TRIGGER && action === this._viewer.root) {
+                alert("You can't paste an action or condition if the selected node is the root object");
+                return;
+            }
+
+            this.loadFromJSON(structure, action);
+            this._viewer.update();
+        }
+
+        /*
+        * Loads a graph from JSON
+        * @pram graph: the graph structure
+        * @param startAction: the action to start load
+        */
+        public loadFromJSON(graph: JSONObject, startAction: Action): void {
+            // If startNode is null, means it replaces all the graph
+            // If not, it comes from a copy/paste
+            if (startAction === null) {
+                for (var i = 0; i < this._viewer.root.children.length; i++) {
+                    this._viewer.removeAction(this._viewer.root.children[i], true);
+                }
+                this._viewer.root.clearChildren();
+            }
+
+            var load = (root: JSONObject, parent: Action, detached: boolean, combine: boolean) => {
+                if (parent === null) {
+                    parent = this._viewer.root;
+                }
+
+                var newAction: Action = null;
+
+                if (root.type !== Type.OBJECT && root.type !== Type.SCENE) {
+                    var action = this._viewer.addAction(parent, root.type, Elements.GetElementFromName(root.name));
+
+                    for (var i = 0; i < root.properties.length; i++) {
+                        var targetType = root.properties[i].targetType;
+                        if (targetType === undefined) {
+                            targetType = "MeshProperties"; // Default is mesh properties
+                        }
+                        action.propertiesResults[i] = { value: root.properties[i].value, targetType: targetType };
+                    }
+
+                    var node = action.node;
+                    node.detached = root.detached;
+
+                    if (detached) {
+                        node.rect.attr("fill", this._viewer.getNodeColor(action.type, detached));
+                    }
+
+                    // If combine array
+                    if (root.combine !== undefined) {
+                        for (var i = 0; i < root.combine.length; i++) {
+                            load(root.combine[i], action, detached, true);
+                        }
+                    }
+
+                    if (!combine) {
+                        parent = parent.children[parent.children.length - 1];
+                    }
+                }
+
+                for (var i = 0; i < root.children.length; i++) {
+                    load(root.children[i], newAction !== null && newAction.combineArray !== null ? newAction.hub : parent, root.detached, false);
+                }
+            };
+
+            // Finish
+            load(graph, startAction, false, false);
+            this._viewer.update();
+        }
+
+        /*
+        * Creates a JSON object starting from a root action
+        * @param root: the root action
+        */
+        public createJSON(root: Action): JSONObject {
+            var action: JSONObject = {
+                type: root.type,
+                name: root.name,
+                detached: root.node.detached,
+                children: new Array<JSONObject>(),
+                combine: new Array<JSONObject>(),
+                properties: new Array<JSONObjectProperty>()
+            };
+
+            // Set properties
+            for (var i = 0; i < root.properties.length; i++) {
+                action.properties.push({
+                    name: root.properties[i].text,
+                    value: root.propertiesResults[i].value,
+                    targetType: root.propertiesResults[i].targetType
+                });
+            }
+
+            // If combine
+            if (root.combineArray !== null) {
+                for (var i = 0; i < root.combineArray.length; i++) {
+                    var combinedAction = root.combineArray[i];
+                    action.combine.push(this.createJSON(combinedAction));
+                }
+
+                root = root.children[0]; // Hub
+            }
+
+            for (var i = 0; i < root.children.length; i++) {
+                action.children.push(this.createJSON(root.children[i]));
+            }
+
+            return action;
+        }
+
+        /*
+        *
+        */
+        public setElementVisible(element: HTMLElement, visible: boolean): void {
+            element.style.display = visible ? "block" : "none";
+        }
+
+    }
+} 

+ 772 - 0
Tools/ActionsBuilder/actionsbuilder.viewer.ts

@@ -0,0 +1,772 @@
+module ActionsBuilder {
+    export interface TraverseResult {
+        action: Action;
+        hit: boolean;
+    }
+
+    export interface UpdateResult {
+        triggerWidth: number;
+        childrenWidths: Array<UpdateResult>;
+    }
+
+    export class Viewer {
+        // Statics
+        private static _NODE_WIDTH: number = 150;
+        private static _NODE_HEIGHT: number = 25;
+        private static _NODE_MINIMIZE_WIDTH: number = 50;
+        private static _VERTICAL_OFFSET: number = 70;
+        
+        private static _DEFAULT_INFO_MESSAGE = "Select or add a node to customize actions";
+        
+        public static get NODE_WIDTH(): number {
+            return Viewer._NODE_WIDTH;
+        }
+
+        public static get NODE_HEIGHT(): number {
+            return Viewer._NODE_HEIGHT;
+        }
+
+        public static get NODE_MINIMIZED_WIDTH(): number {
+            return Viewer._NODE_MINIMIZE_WIDTH;
+        }
+
+        public static get VERTICAL_OFFSET(): number {
+            return Viewer._VERTICAL_OFFSET;
+        }
+
+        // Members
+        // Public
+        public viewerContainer: HTMLElement;
+        public viewerElement: HTMLElement;
+
+        public objectName: string = "Unnamed Object";
+        public zoom: number = 1.0;
+        public mousex: number;
+        public mousey: number;
+
+        public paper: Paper;
+        public root: Action;
+        public selectedNode: Action;
+
+        public parameters: Parameters;
+        public utils: Utils;
+
+        // Private
+        private _toolbar: Toolbar;
+        private _contextMenu: ContextMenu;
+
+        private _firstUpdate: boolean = true;
+
+        /*
+        * Constructor
+        * @param type: the root type object (OBJECT or SCENE)
+        */
+        constructor(type: number) {
+            // Get HTML elements
+            this.viewerContainer = document.getElementById("GraphContainerID");
+            this.viewerElement = document.getElementById("GraphElementID");
+
+            // Create element
+            this.paper = Raphael("GraphElementID", screen.width, screen.height);
+
+            // Configure this
+            //var name = type === Type.OBJECT ? "Unnamed object" : "Scene";
+            this.root = this.addAction(null, type, { name: this.objectName, text: this.objectName, properties: [], description: "" });
+            this.selectedNode = null;
+
+            // Configure events
+            window.addEventListener("resize", (event: Event) => {
+                this.onResize(event);
+            });
+            window.addEventListener("mousemove", (event: MouseEvent) => {
+                this.onMove(event);
+            });
+            this.paper.canvas.addEventListener("click", (event: MouseEvent) => {
+                this.onClick(event);
+            });
+
+            // Load modules
+            this._toolbar = new Toolbar(this);
+            this._contextMenu = new ContextMenu(this);
+
+            this.parameters = new Parameters(this);
+            this.utils = new Utils(this);
+
+            // Finish
+            this.parameters.parametersHelpElement.textContent = Viewer._DEFAULT_INFO_MESSAGE;
+        }
+
+        /*
+        * Resize event
+        * @param event: the resize event
+        */
+        public onResize(event?: Event): void {
+            var tools = document.getElementById("ToolsButtonsID");
+            this.viewerContainer.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 50 + "px";
+            this.viewerElement.style.height = window.innerHeight - tools.getBoundingClientRect().height - 25 - 50 + "px";
+
+            this.parameters.onResize();
+            this._toolbar.onResize();
+
+            if (this.paper.height < window.innerHeight) {
+                this.paper.setSize(this.paper.width, window.innerHeight);
+            }
+
+            if (this._firstUpdate) {
+                this.viewerElement.scrollLeft = ((this.viewerElement.scrollWidth / 2) - (this.viewerElement.getBoundingClientRect().width / 2));
+                this._firstUpdate = false;
+            }
+        }
+
+        /*
+        * Handles the onMove event
+        * @param event: the onMove mouse event
+        */
+        public onMove(event: MouseEvent): void {
+            this.mousex = event.clientX - this.paper.canvas.getBoundingClientRect().left;
+            this.mousey = event.clientY - this.paper.canvas.getBoundingClientRect().top;
+        }
+
+        /*
+        * Handles the onClick event to get selected node
+        * @param event: the onClick mouse event
+        */
+        public onClick(event: MouseEvent): void {
+            if (this._contextMenu.showing) {
+                return;
+            }
+
+            // Reset selected node
+            if (this.selectedNode !== null) {
+                var node = this.selectedNode.node;
+                node.rect.attr("fill", this.getNodeColor(this.selectedNode.type, node.detached));
+            }
+
+            // Configure new selected node
+            var result = this.traverseGraph(null, this.mousex, this.mousey, true);
+            if (result.hit) {
+                this.selectedNode = result.action;
+
+                var node = this.selectedNode.node;
+                node.rect.attr("fill", this.getSelectedNodeColor(this.selectedNode.type, node.detached));
+            }
+            else {
+                this.selectedNode = null;
+                this.parameters.clearParameters();
+                this.parameters.parametersHelpElement.textContent = Viewer._DEFAULT_INFO_MESSAGE;
+            }
+        }
+
+        /*
+        * Set the color theme of the viewer
+        * @param color: the color theme ( ex: "rgb(64, 64, 64)" )
+        */
+        public setColorTheme(color: string): void {
+            this.paper.canvas.style.background = color;
+        }
+
+        /*
+        * Returns the color according to the given parameters
+        * @param action: the action used to select the color
+        * @param detached: if the node is attached to its parent or not
+        */
+        public getNodeColor(type: number, detached: boolean): RaphaelColor {
+            if (detached) {
+                return Raphael.rgb(96, 122, 14);
+            }
+
+            switch (type) {
+                case Type.TRIGGER: return Raphael.rgb(133, 154, 185); break;
+                case Type.ACTION:  return Raphael.rgb(182, 185, 132); break;
+                case Type.FLOW_CONTROL: return Raphael.rgb(185, 132, 140); break;
+                case Type.OBJECT:
+                case Type.SCENE: return Raphael.rgb(255, 255, 255); break;
+                default: break;
+            }
+
+            return null;
+        }
+
+        /*
+        * Returns the selected node color according to the given parameters
+        * @param action: the action used to select the color
+        * @param detached: if the node is attached to its parent or not
+        */
+        public getSelectedNodeColor(type: number, detached: boolean): RaphaelColor {
+            if (detached) {
+                return Raphael.rgb(96, 122, 14);
+            }
+
+            switch (type) {
+                case Type.TRIGGER: return Raphael.rgb(41, 129, 255); break;
+                case Type.ACTION: return Raphael.rgb(255, 220, 42); break;
+                case Type.FLOW_CONTROL: return Raphael.rgb(255, 41, 53); break;
+                case Type.OBJECT:
+                case Type.SCENE: return Raphael.rgb(255, 255, 255); break;
+                default: break;
+            }
+
+            return null;
+        }
+
+        /*
+        * Removes the given action from the graph
+        * @param action: the action to remove
+        * @param removeChildren: if remove the branch or not
+        */
+        public removeAction(action: Action, removeChildren: boolean): void {
+            // If selected node is combine
+            if (action.parent !== null && action.parent.hub === action) {
+                this.removeAction(action.parent, false);
+                return;
+            }
+
+            // Basic suppress
+            this.removeNode(action.node);
+
+            if (action.combineArray !== null) {
+                this.removeNode(action.hub.node);
+                // Remove combine array
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    this.removeNode(action.combineArray[i].node);
+                }
+                action.combineArray.length = 0;
+            }
+
+            if (removeChildren) {
+                for (var i = 0; i < action.children.length; i++) {
+                    this.removeAction(action.children[i], removeChildren);
+                }
+
+                action.clearChildren();
+            }
+            else {
+                for (var i = 0; i < action.children.length; i++) {
+                    action.parent.addChild(action.children[i]);
+                    action.children[i].parent = action.parent;
+                }
+            }
+        }
+
+        /*
+        * Removes the given node (not the action)
+        * @param node: the node to remove
+        */
+        public removeNode(node: Node): void {
+            node.rect.remove();
+            node.text.remove();
+
+            if (node.line !== null) {
+                node.line.remove();
+            }
+        }
+
+        /*
+        * Updates the graph viewer
+        */
+        public update(): void {
+            // Set root position
+            this._setActionPosition(this.root, (this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * this.zoom, 10);
+
+            // Sets node size
+            var onSetNodeSize = (node: Node) => {
+                node.rect.attr("width", node.minimized ? Viewer.NODE_MINIMIZED_WIDTH : Viewer.NODE_WIDTH * this.zoom);
+                node.rect.attr("height", Viewer.NODE_HEIGHT * this.zoom);
+                node.text.attr("font-size", 11 * this.zoom);
+            };
+
+            // First pass: set actions positions according to parents
+            var onSetPositionPass = (action: Action, yPosition: number): void => {
+
+                var node = action.node;
+                var parent = action.parent !== null ? action.parent : null;
+
+                // Set node properties (size, text size, etc.)
+                if (action.combineArray !== null) {
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        var combinedNode = action.combineArray[i].node;
+                        onSetNodeSize(combinedNode);
+                    }
+                }
+                onSetNodeSize(node);
+
+                // Set position from parent
+                if (parent) {
+                    var parentx = parent.node.rect.attr("x");
+                    if (parent.combineArray !== null && parent.combineArray.length > 1) {
+                        parentx += parent.node.rect.attr("width") / 2;
+                    }
+                    this._setActionPosition(action, parentx, yPosition);
+                    this._setActionLine(action);
+                }
+
+                // Calculate total width for current action
+                var totalSize = 0;
+                for (var i = 0; i < action.children.length; i++) {
+                    var childNode = action.children[i].node;
+                    totalSize += childNode.rect.attr("width");
+                }
+
+                // Get values to place nodes according to the parent position
+                var nodeWidth = node.rect.attr("width");
+                var startingPositionX = node.rect.attr("x");
+
+                // Set children positions
+                for (var i = 0; i < action.children.length; i++) {
+                    var childAction = action.children[i];
+                    var childNode = childAction.node;
+
+                    var newPositionX = startingPositionX;
+                    if (childAction.combineArray !== null && childAction.combineArray.length > 1) {
+                        newPositionX -= (childNode.rect.attr("width") / 2) - nodeWidth / 2;
+                    }
+
+                    var newPositionY = yPosition + Viewer.VERTICAL_OFFSET * this.zoom;
+
+                    onSetPositionPass(childAction, newPositionY);
+
+                    this._setActionPosition(childAction, newPositionX, newPositionY);
+                    this._setActionLine(childAction);
+                }
+            };
+
+            onSetPositionPass(this.root, 10 * this.zoom);
+
+            // Seconds pass, get sizes of groups
+            var onGetSizePass = (action: Action, maxSize: number): number => {
+                var mySize = 0;
+
+                if (action.combineArray !== null) {
+                    for (var i = 0; i < action.combineArray.length; i++) {
+                        mySize += action.combineArray[i].node.rect.attr("width");
+                    }
+                }
+                else {
+                    mySize = action.node.rect.attr("width");
+                }
+
+                if (mySize > maxSize) {
+                    maxSize = mySize;
+                }
+
+                for (var i = 0; i < action.children.length; i++) {
+                    maxSize = onGetSizePass(action.children[i], maxSize);
+                }
+
+                return maxSize;
+            };
+
+            // Resize canvas
+            var onResizeCanvas = (action: Action): void => {
+                var node = action.node;
+                var nodex = node.rect.attr("x");
+                var nodey = node.rect.attr("y");
+
+                if (nodex < 0 || nodex > this.paper.width) {
+                    this.paper.setSize(this.paper.width + 1000, this.paper.height);
+                    this._setActionPosition(this.root, (this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * this.zoom, 10);
+                }
+                if (nodey > this.paper.height) {
+                    this.paper.setSize(this.paper.width, this.paper.height + 1000);
+                    this._setActionPosition(this.root, (this.paper.width / 2) - (Viewer.NODE_WIDTH / 2) * this.zoom, 10);
+                }
+            };
+
+            var widths = new Array<UpdateResult>();
+
+            for (var i = 0; i < this.root.children.length; i++) {
+                var trigger = this.root.children[i];
+                var triggerResult: UpdateResult = { triggerWidth: onGetSizePass(trigger, 0), childrenWidths: new Array<UpdateResult>() };
+
+                if (trigger.children.length > 0) {
+                    triggerResult.triggerWidth = 0;
+                }
+
+                for (var j = 0; j < trigger.children.length; j++) {
+                    var actionWidth = onGetSizePass(trigger.children[j], 0);
+
+                    triggerResult.triggerWidth += actionWidth + 15;
+
+                    triggerResult.childrenWidths.push({
+                        triggerWidth: actionWidth,
+                        childrenWidths: null
+                    });
+                }
+
+                widths.push(triggerResult);
+            }
+
+            // Third pass, set positions of nodes
+            var onSetNodePosition = (action: Action, widthArray: Array<UpdateResult>, isChild: boolean) => {
+                var actionsCount = action.children.length;
+                var actionsMiddle = actionsCount % 2;
+                var actionsHasMiddle = actionsMiddle !== 0;
+                var actionsLeftOffset = 0;
+                var actionsRightOffset = 0;
+                var actionWidth = action.node.rect.attr("width");
+
+                if (actionsHasMiddle && actionsCount > 1) {
+                    var middle = Math.floor(actionsCount / 2);
+                    actionsLeftOffset += widthArray[middle].triggerWidth / 2;
+                    actionsRightOffset += widthArray[middle].triggerWidth / 2;
+                }
+
+                // Move left
+                var leftStart = actionsHasMiddle ? Math.floor(actionsCount / 2) - 1 : (actionsCount / 2) - 1;
+                for (var i = leftStart; i >= 0; i--) {
+                    var child = action.children[i];
+                    var node = child.node;
+                    var width = (widthArray[i].triggerWidth) + 15;
+
+                    this._setActionPosition(action.children[i], node.rect.attr("x") - actionsLeftOffset - (width / 2), node.rect.attr("y"));
+                    this._setActionLine(child);
+                    onResizeCanvas(child);
+
+                    actionsLeftOffset += width;
+                }
+
+                // Move right
+                var rightStart = actionsHasMiddle ? Math.round(actionsCount / 2) : actionsCount / 2;
+                for (var i = rightStart; i < actionsCount; i++) {
+                    var child = action.children[i];
+                    var node = child.node;
+                    var width = (widthArray[i].triggerWidth) + 15;
+
+                    this._setActionPosition(action.children[i], node.rect.attr("x") + actionsRightOffset + (width / 2), node.rect.attr("y"));
+                    this._setActionLine(child);
+                    onResizeCanvas(child);
+
+                    actionsRightOffset += width;
+                }
+            };
+
+            onSetNodePosition(this.root, widths, false);
+            for (var i = 0; i < this.root.children.length; i++) {
+                onSetNodePosition(this.root.children[i], widths[i].childrenWidths, true);
+            }
+            
+        }
+
+        /*
+        * Adds an action to the graph viewer and returns it
+        * @param parent: the parent action
+        * @param type: the action type
+        * @param element: the Actions Builder type (TRIGGERS, ACTIONS, FLOW_CONTROLS)
+        */
+        public addAction(parent: Action, type: number, element: Element): Action {
+            var node = this._createNode(element.text, type, parent === null);
+            var action = new Action(node);
+
+            if (element.name === "CombineAction") {
+                action.combineArray = new Array<Action>();
+
+                var hubElement = Elements.FLOW_CONTROLS[Elements.FLOW_CONTROLS.length - 1];
+                var hub = this.addAction(action, Type.FLOW_CONTROL, hubElement);
+
+                action.hub = hub;
+                action.addChild(hub);
+                this._createActionAnimation(hub);
+            }
+
+            action.name = element.name;
+            action.properties = element.properties;
+            action.type = type;
+
+            // Configure properties
+            for (var i = 0; i < action.properties.length; i++) {
+                action.propertiesResults.push({ targetType: action.properties[i].targetType, value: action.properties[i].value });
+            }
+
+            if (action.properties !== null && action.properties.length > 0) {
+                if (action.properties[0].text === "target") {
+                    action.propertiesResults[0].value = this.objectName;
+                }
+            }
+
+            if (parent !== null) {
+                if (parent.combineArray === null) {
+                    parent.addChild(action);
+                }
+                else if (parent.combineArray !== null && action.name !== "Hub") {
+                    parent.combineArray.push(action);
+                    action.parent = parent;
+                    action.combineAction = parent;
+                    parent.node.text.attr("text", "");
+                }
+            }
+
+            // Create animation
+            this._createActionAnimation(action);
+
+            return action;
+        }
+
+        /*
+        * Traverses the graph viewer and returns if an action
+        * is selected at coordinates (x, y)
+        * @param start: the start node. Can be null
+        * @param x: the x coordinate
+        * @param y: the y coordinate
+        * @param traverseCombine: if we traverse combine actions children
+        */
+        public traverseGraph(start: Action, x: number, y: number, traverseCombine: boolean): TraverseResult {
+            if (start === null) start = this.root;
+
+            var result: TraverseResult = { action: start, hit: true };
+
+            if (start.node.isPointInside(x, y)) {
+                return result;
+            }
+
+            for (var i = 0; i < start.children.length; i++) {
+                var action = start.children[i];
+
+                if (action.node.isPointInside(x, y)) {
+                    result.hit = true;
+                    result.action = start.children[i];
+
+                    if (traverseCombine && action.combineArray !== null) {
+                        for (var j = 0; j < action.combineArray.length; j++) {
+                            if (action.combineArray[j].node.isPointInside(x, y)) {
+                                result.action = action.combineArray[j];
+                                break;
+                            }
+                        }
+                    }
+
+                    return result;
+                }
+
+                result = this.traverseGraph(action, x, y, traverseCombine);
+                if (result.hit) {
+                    return result;
+                }
+            }
+
+            result.hit = false;
+            result.action = null;
+            return result;
+        }
+
+        /*
+        * Sets the action's position (node)
+        * @param action: the action to place
+        * @param x: the x position of the action
+        * @param y: the y position of the action
+        */
+        public _setActionPosition(action: Action, x: number, y: number): void {
+            var node = action.node;
+            var offsetx = node.rect.attr("x") - x;
+            var parent = action.parent;
+
+            if (parent !== null && parent.combineArray !== null && parent.combineArray.length > 1) {
+                var parentNode = parent.node;
+                x = parentNode.rect.attr("x") + (parent.node.rect.attr("width") / 2) - (node.rect.attr("width") / 2);
+            }
+            
+            node.rect.attr("x", x);
+            node.rect.attr("y", y);
+
+            var textBBox = node.text.getBBox();
+            var textWidth = 0;
+            if (textBBox !== null && textBBox !== undefined) {
+                textWidth = textBBox.width;
+            }
+
+            node.text.attr("x", x + node.rect.attr("width") / 2 - textWidth / 2);
+            node.text.attr("y", y + node.rect.attr("height") / 2);
+
+            if (action.combineArray !== null && action.combineArray.length > 0) {
+                var length = 0;
+
+                for (var i = 0; i < action.combineArray.length; i++) {
+                    var combinedAction = action.combineArray[i];
+                    var combinedNode = combinedAction.node;
+
+                    combinedNode.rect.attr("x", node.rect.attr("x") + length);
+                    combinedNode.rect.attr("y", node.rect.attr("y"));
+
+                    textBBox = combinedNode.text.getBBox();
+                    if (textBBox !== null) {
+                        textWidth = textBBox.width;
+                    }
+                    combinedNode.text.attr("x", combinedNode.rect.attr("x") + combinedNode.rect.attr("width") / 2 - textWidth / 2);
+                    combinedNode.text.attr("y", y + combinedNode.rect.attr("height") / 2);
+
+                    length += combinedNode.rect.attr("width");
+                }
+
+                node.rect.attr("width", length);
+            }
+
+            for (var i = 0; i < action.children.length; i++) {
+                var child = action.children[i];
+                this._setActionPosition(child, child.node.rect.attr("x") - offsetx, y + Viewer.VERTICAL_OFFSET * this.zoom);
+                this._setActionLine(child);
+            }
+        }
+
+        /*
+        * Configures the line (link) between the action and its parent
+        * @param action: the action to configure
+        */
+        private _setActionLine(action: Action): void {
+            if (action.node.line === null) {
+                return;
+            }
+
+            var node = action.node;
+            var nodex = node.rect.attr("x");
+            var nodey = node.rect.attr("y");
+            var nodeWidth = node.rect.attr("width");
+            var nodeHeight = node.rect.attr("height");
+
+            var parent = action.parent.node;
+            var parentx = parent.rect.attr("x");
+            var parenty = parent.rect.attr("y");
+            var parentWidth = parent.rect.attr("width");
+            var parentHeight = parent.rect.attr("height");
+
+            if (node.detached) {
+                node.line.attr("path", ["M", nodex, nodey, "L", nodex, nodey]);
+                return;
+            }
+
+            var line1x = nodex + (nodeWidth / 2);
+            var line1y = nodey;
+
+            var line2y = line1y - (line1y - parenty - parentHeight) / 2;
+            var line3x = parentx + (parentWidth / 2);
+            var line4y = parenty + parentHeight;
+
+            node.line.attr("path", ["M", line1x, line1y, "L", line1x, line2y, "L", line3x, line2y, "L", line3x, line4y]);
+        }
+
+        /*
+        * Creates and returns a node
+        * @param text: the text to draw in the nde
+        * @param color: the node's color
+        * @param noLine: if draw a line to the parent or not
+        */
+        public _createNode(text: string, type: number, noLine: boolean): Node {
+            var node = new Node();
+            var color = this.getNodeColor(type, false);
+
+            node.rect = this.paper.rect(20, 20, Viewer.NODE_WIDTH, Viewer.NODE_HEIGHT, 0);
+            node.rect.attr("fill", color);
+
+            node.text = this.paper.text(20, 20, text);
+            node.text.attr("font-size", 11);
+            node.text.attr("text-anchor", "start");
+            node.text.attr("font-family", "Sinkin Sans Light");
+
+            if (!noLine) {
+                node.line = this.paper.path("");
+                node.line.attr("stroke", color);
+            }
+
+            return node;
+        }
+
+        /*
+        * Creates the drag animation
+        * @param action: the action to animate
+        */
+        private _createActionAnimation(action: Action): void {
+            var node = action.node;
+            var finished = true;
+            var nodex: number = 0;
+            var nodey: number = 0;
+
+            var onMove = (dx: number, dy: number, x: number, y: number) =>
+            { };
+
+            var onStart = (x: number, y: number, event: MouseEvent) => {
+                if (node.minimized) {
+                    return;
+                }
+
+                if (finished) {
+                    nodex = node.rect.attr("x");
+                    nodey = node.rect.attr("y");
+                }
+                finished = false;
+
+                node.rect.animate({
+                    x: node.rect.attr("x") - 10,
+                    y: node.rect.attr("y"),
+                    width: (Viewer.NODE_WIDTH + 20) * this.zoom,
+                    height: (Viewer.NODE_HEIGHT + 10) * this.zoom,
+                    opacity: 0.25
+                }, 500, ">");
+            };
+
+            var onEnd = (event: MouseEvent) => {
+                if (!node.minimized) {
+                    node.rect.animate({
+                        x: nodex,
+                        y: nodey,
+                        width: Viewer.NODE_WIDTH * this.zoom,
+                        height: Viewer.NODE_HEIGHT * this.zoom,
+                        opacity: 1.0
+                    }, 500, ">", () => { finished = true; });
+                }
+
+                var dragResult = this.traverseGraph(null, this.mousex, this.mousey, true);
+
+                if (dragResult.hit && dragResult.action === action || !dragResult.hit) {
+                    // Create parameters. Action can be null
+                    this.parameters.createParameters(action);
+                }
+                else {
+                    // Manage drag'n'drop
+                    if (dragResult.action.children.length > 0 && action.type !== Type.TRIGGER) {
+                        return;
+                    }
+
+                    if (action.type === Type.TRIGGER && dragResult.action !== this.root) {
+                        return;
+                    }
+
+                    if (action.type === Type.ACTION && dragResult.action === this.root) {
+                        return;
+                    }
+
+                    if (action.type === Type.FLOW_CONTROL && (dragResult.action === this.root || dragResult.action.type === Type.FLOW_CONTROL)) {
+                        return;
+                    }
+
+                    if (action === dragResult.action.parent) {
+                        return;
+                    }
+
+                    if (action.parent !== null && action.parent.combineArray !== null) { // Musn't move hubs of combine actions
+                        return;
+                    }
+
+                    // Reset node
+                    node.rect.stop(node.rect.animation);
+                    node.text.stop(node.text.animation);
+
+                    node.rect.undrag();
+                    node.text.undrag();
+
+                    node.rect.attr("opacity", 1.0);
+                    node.rect.attr("width", Viewer.NODE_WIDTH);
+                    node.rect.attr("height", Viewer.NODE_HEIGHT);
+
+                    if (action.parent !== null) {
+                        // Configure drag'n'drop
+                        action.parent.removeChild(action);
+                        dragResult.action.addChild(action);
+                        this.update();
+                        this._createActionAnimation(action);
+                    }
+                }
+            };
+
+            node.rect.drag(onMove, onStart, onEnd);
+            node.text.drag(onMove, onStart, onEnd);
+        }
+    }
+}

+ 42 - 0
Tools/ActionsBuilder/gulpfile.js

@@ -0,0 +1,42 @@
+var gulp = require('gulp');
+var ts = require('gulp-typescript');
+
+var files = [
+    // Files
+    // Equivalent to "./*.ts",
+    "actionsbuilder.actionNode.ts",
+    "actionsbuilder.contextMenu.ts",
+	"actionsbuilder.list.ts",
+    "actionsbuilder.main.ts",
+    "actionsbuilder.parameters.ts",
+    "actionsbuilder.toolbar.ts",
+    "actionsbuilder.ts",
+    "actionsbuilder.utils.ts",
+    "actionsbuilder.viewer.ts",
+    // References
+    "raphaeljs.d.ts",
+    "../../dist/*.d.ts",
+    "../..external references/**/*.d.ts"
+];
+
+gulp.task("default", function () {
+    var result = gulp.src(files)
+		.pipe(ts({
+            target: "ES5",
+		    out: "actionsbuilder.max.js" // Merge
+		}));
+    return result.js.pipe(gulp.dest("./Sources/"));
+});
+
+gulp.task("debug", function () {
+    var result = gulp.src(files)
+		.pipe(ts({
+		    target: "ES5",
+            outDir: "./Sources/"
+		}));
+    return result.js.pipe(gulp.dest("./Sources/"));
+});
+
+gulp.task("watch", function () {
+    gulp.watch(files, ["default"]);
+});

+ 11 - 0
Tools/ActionsBuilder/package.json

@@ -0,0 +1,11 @@
+{
+  "name": "BabylonJSActionsBuilder",
+  "version": "2.2.0",
+  "description": "",
+  "main": "",
+  "devDependencies": {
+    "gulp": "^3.8.11",
+    "typescript": "~1.5.3",
+    "gulp-typescript": "~2.8.0"
+  }
+}

+ 69 - 0
Tools/ActionsBuilder/raphaeljs.d.ts

@@ -0,0 +1,69 @@
+/*
+Raphael.js declarations
+*/
+
+// Raphael.js bounding box interface
+interface RaphaelBoundingBox {
+    width: number;
+    height: number;
+}
+
+// Raphael.js animation interface
+interface RaphaelAnimation
+{ }
+
+// Raphael.js element interface
+interface RaphaelElement {
+    attr(attribute: string, value?: any): any;
+
+    remove(): void;
+
+    hide(): void;
+    show(): void;
+
+    isPointInside(x: number, y: number): boolean;
+    getBBox(): RaphaelBoundingBox;
+
+    animate(attributes: any, time: number, type: string, callback?: () => void): void;
+    stop(animation: RaphaelAnimation);
+    animation: RaphaelAnimation;
+
+    click(onClick: (event: MouseEvent) => void);
+    drag(onMove: (dx: number, dy: number, x: number, y: number) => void, onStart: (x: number, y: number, event: MouseEvent) => void, onEnd: (event: MouseEvent) => void): void;
+    undrag(): void;
+}
+
+// Raphael.js rect interface
+interface Rect extends RaphaelElement
+{ }
+
+// Raphael.js text interface
+interface Text extends RaphaelElement
+{ }
+
+// Raphael.js path interface
+interface Path extends RaphaelElement
+{ }
+
+// Raphael.js rgb interface
+interface RaphaelColor
+{ }
+
+// Raphael.js paper interface
+interface Paper {
+    canvas: HTMLCanvasElement;
+
+    width: number;
+    height: number;
+    setSize(width: number, height: number);
+
+    rect(x: number, y: number, width: number, height: number, r?: number): Rect;
+    text(x: number, y: number, text: string): Text;
+    path(pathString: string);
+}
+
+// Raphael.js
+declare var Raphael: {
+    (containerID: string, width: number, height: number): Paper;
+    rgb(r: number, g: number, b: number): RaphaelColor;
+}