Bläddra i källkod

Merge pull request #696 from julien-moreau/master

Adding Actions Builder with the README.md file
David Catuhe 10 år sedan
förälder
incheckning
b7cf6a6973
50 ändrade filer med 54893 tillägg och 0 borttagningar
  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. 48 0
      Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans_NOTICE.txt
  35. 91 0
      Tools/ActionsBuilder/Sources/index-debug.html
  36. 191 0
      Tools/ActionsBuilder/Sources/index.css
  37. 72 0
      Tools/ActionsBuilder/Sources/index.html
  38. 11 0
      Tools/ActionsBuilder/Sources/raphael.js
  39. 101 0
      Tools/ActionsBuilder/actionsbuilder.actionNode.ts
  40. 149 0
      Tools/ActionsBuilder/actionsbuilder.contextMenu.ts
  41. 301 0
      Tools/ActionsBuilder/actionsbuilder.list.ts
  42. 105 0
      Tools/ActionsBuilder/actionsbuilder.main.ts
  43. 551 0
      Tools/ActionsBuilder/actionsbuilder.parameters.ts
  44. 94 0
      Tools/ActionsBuilder/actionsbuilder.toolbar.ts
  45. 326 0
      Tools/ActionsBuilder/actionsbuilder.ts
  46. 527 0
      Tools/ActionsBuilder/actionsbuilder.utils.ts
  47. 772 0
      Tools/ActionsBuilder/actionsbuilder.viewer.ts
  48. 42 0
      Tools/ActionsBuilder/gulpfile.js
  49. 11 0
      Tools/ActionsBuilder/package.json
  50. 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();
+};

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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 = {}));

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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


+ 48 - 0
Tools/ActionsBuilder/Sources/fonts/SinkinSans/SinkinSans_NOTICE.txt

@@ -0,0 +1,48 @@
+
+=== SINKIN SANS 100 THIN ===
+=== SINKIN SANS 100 THIN ITALIC ===
+=== SINKIN SANS 200 X LIGHT ===
+=== SINKIN SANS 200 X LIGHT ITALIC ===
+=== SINKIN SANS 300 LIGHT ===
+=== SINKIN SANS 300 LIGHT ITALIC ===
+=== SINKIN SANS 400 REGULAR ===
+=== SINKIN SANS 400 ITALIC ===
+=== SINKIN SANS 500 MEDIUM ===
+=== SINKIN SANS 500 MEDIUM ITALIC ===
+=== SINKIN SANS 600 SEMIBOLD ===
+=== SINKIN SANS 600 SEMIBOLD ITALIC ===
+=== SINKIN SANS 700 BOLD ===
+=== SINKIN SANS 700 BOLD ITALIC ===
+=== SINKIN SANS 800 BLACK ===
+=== SINKIN SANS 800 BLACK ITALIC ===
+=== SINKIN SANS 900 X BLACK ===
+=== SINKIN SANS 900 X BLACK ITALIC ===
+
+Keith Bates / K-Type © 2014 (version 1.1)
+www.k-type.com  -  info@k-type.com
+
+SINKIN SANS is a simple, pleasantly proportioned sans serif with tiny, inconspicuous notches that sink into verticals at the intersections of strokes adding highlights to congested corners. The incisions make right angles appear sharper and improve definition in more intricate glyphs. Although the nicks are barely noticeable they add emphasis to outlines and give a lift to shady nooks.
+
+------------------------------------------------
+
+== Licence Information ==
+
+All weights of SINKIN SANS are totally free for commercial and personal use as desktop and web fonts. For @font-face users, a link to www.k-type.com somewhere on your site is requested, elsewhere a credit to K-Type is appreciated.
+
+You can freely distribute and modify SINKIN SANS provided the original copyright, license information and details of any modifications are included. SINKIN SANS is gifted 'as is', without warranty. SINKIN SANS is a trademark of K-Type. Licensed under the Apache License, Version 2.0
+
+http://www.apache.org/licenses/LICENSE-2.0.html
+
+------------------------------------------------
+
+== Installing Fonts ==
+
+Fonts are placed in your operating system's Fonts folder and will be made available to all the applications or programs you use.
+
+= Windows =
+Put the .ttf or .otf font file into C:\Windows\Fonts, or right-click on the font files > Install
+
+= Mac =
+Put the .ttf or .otf font file into /Library/Fonts
+
+------------------------------------------------

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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;
+}