Can now add nodes/edges
authorveltr
Fri, 27 Jul 2012 19:15:32 +0200
changeset 4 f5297dde9053
parent 3 7722ec70c01b
child 5 67085e6281e5
Can now add nodes/edges
.hgignore
client/data/dynamic-data.json
client/data/simple-persist.php
client/js/json-serializer.js
client/js/main.js
client/js/model.js
client/js/paper-renderer.js
client/js/random-data.js
client/render-test.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Jul 27 19:15:32 2012 +0200
@@ -0,0 +1,3 @@
+
+syntax: regexp
+^\.project$
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/data/dynamic-data.json	Fri Jul 27 19:15:32 2012 +0200
@@ -0,0 +1,112 @@
+{
+    "title": "Test Graph",
+    "creation_date": "2012-07-25T11:00:00.0Z",
+    "users": [
+        {
+            "id": "u-cybunk",
+            "title": "Samuel",
+            "uri": "http://twitter.com/cybunk",
+            "color": "#e00000"
+        },
+        {
+            "id": "u-raphv",
+            "title": "Raphael",
+            "uri": "http://twitter.com/raphv",
+            "color": "#00a000"
+        }
+    ],
+    "nodes": [
+        {
+            "id": "n-001",
+            "title": "連環 (Renkan)",
+            "uri": "http://ja.wikipedia.org/wiki/%E7%99%BE%E5%AD%A6%E9%80%A3%E7%92%B0",
+            "created_by": "u-cybunk",
+            "position": {
+                "x": 0,
+                "y": 0
+            }
+        },
+        {
+            "id": "n-002",
+            "title": "Savoir",
+            "uri": "http://fr.wikipedia.org/wiki/Savoir",
+            "created_by": "u-raphv",
+            "position": {
+                "x": 200,
+                "y": 50
+            }
+        },
+        {
+            "id": "n-003",
+            "title": "Connaissance",
+            "uri": "http://fr.wikipedia.org/wiki/Connaissance",
+            "created_by": "u-raphv",
+            "position": {
+                "x": 300,
+                "y": -50
+            }
+        },
+        {
+            "id": "n-004",
+            "title": "graphe",
+            "uri": "http://fr.wikipedia.org/wiki/Th%C3%A9orie_des_graphes",
+            "created_by": "u-cybunk",
+            "position": {
+                "x": -200,
+                "y": 0
+            }
+        },
+        {
+            "id": "n-005",
+            "title": "nœud",
+            "uri": "http://fr.wikipedia.org/wiki/Th%C3%A9orie_des_graphes",
+            "created_by": "u-cybunk",
+            "position": {
+                "x": -350,
+                "y": 100
+            }
+        },
+        {
+            "id": "n-006",
+            "title": "lien",
+            "uri": "http://fr.wikipedia.org/wiki/Th%C3%A9orie_des_graphes",
+            "created_by": "u-cybunk",
+            "position": {
+                "x": -300,
+                "y": -100
+            }
+        }
+    ],
+    "edges": [
+        {
+            "id": "e-001",
+            "from": "n-001",
+            "to": "n-002",
+            "created_by": "u-raphv"
+        },
+        {
+            "id": "e-002",
+            "from": "n-002",
+            "to": "n-003",
+            "created_by": "u-raphv"
+        },
+        {
+            "id": "e-003",
+            "from": "n-001",
+            "to": "n-004",
+            "created_by": "u-cybunk"
+        },
+        {
+            "id": "e-004",
+            "from": "n-004",
+            "to": "n-005",
+            "created_by": "u-cybunk"
+        },
+        {
+            "id": "e-005",
+            "from": "n-004",
+            "to": "n-006",
+            "created_by": "u-cybunk"
+        }
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/data/simple-persist.php	Fri Jul 27 19:15:32 2012 +0200
@@ -0,0 +1,14 @@
+<?php
+    header('Content-Type: application/json; charset=utf-8');
+    $file = "dynamic-data.json";
+    if (isset($_GET['reset']) && $_GET['reset']) {
+        $resetfile = "test-data.json";
+        file_put_contents($file, file_get_contents($resetfile));
+    }
+    if ($_POST) {
+        file_put_contents($file, json_encode($_POST));
+        echo json_encode(array("result" => "OK"));
+    } else {
+        echo file_get_contents($file);
+    }
+?>
\ No newline at end of file
--- a/client/js/json-serializer.js	Fri Jul 27 12:22:10 2012 +0200
+++ b/client/js/json-serializer.js	Fri Jul 27 19:15:32 2012 +0200
@@ -2,6 +2,10 @@
 
 Rkns.Serializers.BasicJson.prototype._init = function() {
     this.load(this._project._opts.url);
+    var _this = this;
+    this.save = Rkns._.throttle(function() {
+        _this._save.apply(this, Array.prototype.slice.call(arguments,0));
+    }, 2000);
 }
 
 Rkns.Serializers.BasicJson.prototype.load = function(_url) {
@@ -19,43 +23,89 @@
     var _proj = this._project;
     _proj.title = _serializedData.title || "(untitled project)";
     if (typeof _serializedData.users === "object" && _serializedData.users) {
-        _proj.users.addElements(
-            Rkns._(_serializedData.users).map(function(_data) {
-                var _userData = {
-                    id: _data.id,
-                    title: _data.title,
-                    uri: _data.uri,
-                    color: _data.color
-                };
-                return new Rkns.Model.User(_proj, _userData);
-            })
-        );
+        Rkns._(_serializedData.users).each(function(_data) {
+            var _userData = {
+                id: _data.id,
+                title: _data.title,
+                uri: _data.uri,
+                color: _data.color
+            };
+            _proj.addUser(_userData);
+        });
     }
     if (typeof _serializedData.nodes === "object" && _serializedData.nodes) {
-        _proj.nodes.addElements(
-            Rkns._(_serializedData.nodes).map(function(_data) {
-                var _nodeData = {
-                    id: _data.id,
-                    title: _data.title,
-                    uri: _data.uri,
-                    created_by: _data.created_by,
-                    position: {
-                        x: _data.position.x,
-                        y: _data.position.y
-                        //x: 800 * Math.random() - 400,
-                        //y: 600 * Math.random() - 300
-                    }
-                };
-                return new Rkns.Model.Node(_proj, _nodeData);
-            })
-        );
+        Rkns._(_serializedData.nodes).each(function(_data) {
+            var _nodeData = {
+                id: _data.id,
+                title: _data.title,
+                uri: _data.uri,
+                created_by: _data.created_by,
+                position: {
+                    x: _data.position.x,
+                    y: _data.position.y
+                }
+            };
+            _proj.addNode(_nodeData);
+        });
     }
     if (typeof _serializedData.edges === "object" && _serializedData.edges) {
-        _proj.edges.addElements(
-            Rkns._(_serializedData.edges).map(function(_data) {
-                var _edgeData = _data;
-                return new Rkns.Model.Edge(_proj, _edgeData);
-            })
-        );
+        Rkns._(_serializedData.edges).each(function(_data) {
+            var _edgeData = {
+                id: _data.id,
+                title: _data.title,
+                uri: _data.uri,
+                from: _data.from,
+                to: _data.to,
+                created_by: _data.created_by
+            };
+            _proj.addEdge(_edgeData);
+        });
     }
 }
+
+Rkns.Serializers.BasicJson.prototype.serialize = function() {
+    var _res = {
+        title: this._project.title,
+        users: this._project.users.map(function(_user) {
+            return {
+                id: _user.id,
+                title: _user.title,
+                uri: _user.uri,
+                color: _user.color
+            }
+        }),
+        nodes: this._project.nodes.map(function(_node) {
+            return {
+                id: _node.id,
+                title: _node.title,
+                uri: _node.uri,
+                created_by: _node.created_by.id,
+                position: {
+                    x: _node.position.x,
+                    y: _node.position.y
+                }
+            }
+        }),
+        edges: this._project.edges.map(function(_node) {
+            return {
+                id: _node.id,
+                title: _node.title,
+                uri: _node.uri,
+                from: _node.from.id,
+                to: _node.to.id,
+                created_by: _node.created_by.id
+            }
+        })
+    }
+    return _res;
+}
+
+Rkns.Serializers.BasicJson.prototype._save = function() {
+    var _data = this.serialize();
+    Rkns.$.post(
+        this._project._opts.url,
+        _data,
+        function(_res) {
+        }
+    );
+}
--- a/client/js/main.js	Fri Jul 27 12:22:10 2012 +0200
+++ b/client/js/main.js	Fri Jul 27 19:15:32 2012 +0200
@@ -19,8 +19,13 @@
 /* Declaring the Renkan Namespace Rkns */
 
 Rkns = {
-    _FROM_GRAPHICS: 0,
-    _FROM_DATA: 1
+    _NODE_RADIUS: 20,
+    _NODE_FONT_SIZE: 14,
+    _ARROW_LENGTH: 20,
+    _ARROW_WIDTH: 15,
+    _RENDER: 1,
+    _SAVE: 2,
+    _RENDER_AND_SAVE: 3
 }
 
 Rkns.$ = jQuery;
@@ -61,6 +66,8 @@
     }
 }
 
+Rkns.Serializers._Base.prototype.save = function() {}
+
 Rkns.Renderers = {};
 
 Rkns.Renderers._Base = function(_project) {
@@ -80,14 +87,52 @@
     this.users = new Rkns.Model.List();
     this.nodes = new Rkns.Model.List();
     this.edges = new Rkns.Model.List();
+    if (typeof this._opts.user === "object") {
+        this.current_user = this.addUser(this._opts.user)
+    }
     this.serializer = new Rkns.Serializers[_opts.serializer](this);
     this.renderer = new Rkns.Renderers[_opts.renderer](this);
     var _this = this;
     this.serializer.onLoad(function() {
+        if (typeof _this.current_user === "undefined") {
+            _this.current_user = _proj.users[0];
+        }
         _this.renderer.draw();
     });
 }
 
+Rkns.Project.prototype.addNode = function(_props, _render_save) {
+    var _node = new Rkns.Model.Node(this, _props);
+    this.nodes.push(_node);
+    if (typeof _render_save !== "undefined" && (_render_save&Rkns._RENDER)) {
+        var _controller = this.renderer.addElement("Node", _node);
+        _controller.redraw();
+    }
+    if (typeof _render_save !== "undefined" && (_render_save&Rkns._SAVE)) {
+        this.serializer.save();
+    }
+    return _node;
+}
+
+Rkns.Project.prototype.addEdge = function(_props, _render_save) {
+    var _edge = new Rkns.Model.Edge(this, _props);
+    this.edges.push(_edge);
+    if (typeof _render_save !== "undefined" && (_render_save&Rkns._RENDER)) {
+        var _controller = this.renderer.addElement("Edge", _edge);
+        _controller.redraw();
+    }
+    if (typeof _render_save !== "undefined" && (_render_save&Rkns._SAVE)) {
+        this.serializer.save();
+    }
+    return _edge;
+}
+
+Rkns.Project.prototype.addUser = function(_props, _render_save) {
+    var _user = new Rkns.Model.User(this, _props);
+    this.users.push(_user);
+    return _user;
+}
+
 /* Utility functions */
 
 Rkns.Utils = {
--- a/client/js/model.js	Fri Jul 27 12:22:10 2012 +0200
+++ b/client/js/model.js	Fri Jul 27 19:15:32 2012 +0200
@@ -3,35 +3,30 @@
 Rkns.Model._BaseElement = function(_project, _props) {
     if (typeof _props !== "undefined") {
         this._project = _project;
-        this.id = _props.id || Rkns.utils.getUID(this.type);
+        this.id = _props.id || Rkns.Utils.getUID(this.type);
         this.title = _props.title || "(untitled " + this.type + ")";
         this.description = _props.description || "";
         this.uri = _props.uri || "";
     }
 }
 
-Rkns.Model._BaseElement.prototype.addReference = function(_propName, _list, _id) {
-    this[ _propName + "_id" ] = _id;
+Rkns.Model._BaseElement.prototype.addReference = function(_propName, _list, _id, _default) {
     var _element = _list.getElement(_id);
-    this[ _propName ] = _element;
+    if (typeof _element === "undefined" && typeof _default !== "undefined") {
+        this[ _propName ] = _default;
+        this[ _propName + "_id" ] = _default.id;
+    } else {
+        this[ _propName + "_id" ] = _id;
+        this[ _propName ] = _element;
+    }
 }
 
 Rkns.Model._BaseElement.prototype.updateGraphics = function() {
-    this._project._renderer.redraw();
+    this._project.renderer.redraw();
 }
 
 Rkns.Model._BaseElement.prototype.updateData = function() {
-}
-
-Rkns.Model._BaseElement.prototype.propagateChanges = function(_from) {
-    switch(_from) {
-        case Rkns._FROM_GRAPHICS:
-            this.updateData();
-        break;
-        case Rkns._FROM_DATA:
-            this.updateGraphics();
-        break;
-    }
+    this._project.serializer.save(this);
 }
 
 /* Element Class Generator */
@@ -55,11 +50,11 @@
 Rkns.Model.Node = Rkns.Model._elementClass("node");
 
 Rkns.Model.Node.prototype._init = function(_project, _props) {
-    this.addReference("created_by", this._project.users, _props.created_by);
+    this.addReference("created_by", this._project.users, _props.created_by, _project.current_user);
     this.position = _props.position;
 }
 
-Rkns.Model.Node.prototype.setPosition = function(_from, _x, _y) {
+Rkns.Model.Node.prototype.setPosition = function(_x, _y) {
     if (typeof _x === "object") {
         if (typeof _x.x !== "undefined" && typeof _x.y !== "undefined") {
             this.position.x = _x.x;
@@ -76,7 +71,6 @@
             this.position.y = +_y;
         }
     }
-    this.propagateChanges(_from);
 }
 
 /* Edge Model */
@@ -84,7 +78,7 @@
 Rkns.Model.Edge = Rkns.Model._elementClass("edge");
 
 Rkns.Model.Edge.prototype._init = function(_project, _props) {
-    this.addReference("created_by", this._project.users, _props.created_by);
+    this.addReference("created_by", this._project.users, _props.created_by, _project.current_user);
     this.addReference("from", this._project.nodes, _props.from);
     this.addReference("to", this._project.nodes, _props.to);
 }
--- a/client/js/paper-renderer.js	Fri Jul 27 12:22:10 2012 +0200
+++ b/client/js/paper-renderer.js	Fri Jul 27 19:15:32 2012 +0200
@@ -4,28 +4,32 @@
 
 Rkns.Renderers.Paper__Controllers._Base = function(_renderer, _element) {
     if (typeof _renderer !== "undefined") {
+        this.id = Rkns.Utils.getUID('controller');
         this._renderer = _renderer;
         this._element = _element;
         this._element.__controller = this;
     }
 }
 
+Rkns.Renderers.Paper__Controllers._Base.prototype.select = function() {}
+
+Rkns.Renderers.Paper__Controllers._Base.prototype.unselect = function() {}
+
 Rkns.Renderers.Paper__Controllers.Node = Rkns.Utils.inherit(Rkns.Renderers.Paper__Controllers._Base);
 
 Rkns.Renderers.Paper__Controllers.Node.prototype._init = function() {
     this._renderer.node_layer.activate();
     this.type = "node";
-    this.node_circle = new paper.Path.Circle([0, 0], 20);
+    this.node_circle = new paper.Path.Circle([0, 0], Rkns._NODE_RADIUS);
     this.node_circle.fillColor = '#ffffff';
     this.node_circle.__controller = this;
     this.node_text = new paper.PointText([0,0]);
     this.node_text.characterStyle = {
-        fontSize: 14,
+        fontSize: Rkns._NODE_FONT_SIZE,
         fillColor: 'black'
     };
     this.node_text.paragraphStyle.justification = 'center';
     this.node_text.__controller = this;
-    this.redraw();
 }
 
 Rkns.Renderers.Paper__Controllers.Node.prototype.redraw = function() {
@@ -33,20 +37,28 @@
     this.node_paper_coords = this._renderer.toPaperCoords(this.node_model_coords);
     this.node_circle.position = this.node_paper_coords;
     this.node_text.content = this._element.title;
-    this.node_text.position = this.node_paper_coords.add([0, 35]);
+    this.node_text.position = this.node_paper_coords.add([0, 2 * Rkns._NODE_RADIUS]);
     this.node_circle.strokeColor = this._element.created_by.color;
 }
 
 Rkns.Renderers.Paper__Controllers.Node.prototype.paperShift = function(_delta) {
-    this._element.setPosition(Rkns._FROM_GRAPHICS, this._renderer.toModelCoords(this.node_paper_coords.add(_delta)));
+    this._element.setPosition(this._renderer.toModelCoords(this.node_paper_coords.add(_delta)));
+    this._renderer._project.serializer.save();
     this._renderer.redraw();
 }
 
+Rkns.Renderers.Paper__Controllers.Node.prototype.select = function(_delta) {
+    this.node_circle.strokeWidth = 3;
+}
+
+Rkns.Renderers.Paper__Controllers.Node.prototype.unselect = function(_delta) {
+    this.node_circle.strokeWidth = 1;
+}
+
 /* */
 
 Rkns.Renderers.Paper__Controllers.Edge = Rkns.Utils.inherit(Rkns.Renderers.Paper__Controllers._Base);
 
-
 Rkns.Renderers.Paper__Controllers.Edge.prototype._init = function() {
     this._renderer.edge_layer.activate();
     this.type = "edge";
@@ -55,33 +67,51 @@
     this.edge_line = new paper.Path();
     this.edge_line.add([0,0],[0,0]);
     this.edge_line.__controller = this;
+    this.edge_arrow = new paper.Path();
+    this.edge_arrow.add([0,0],[Rkns._ARROW_LENGTH,Rkns._ARROW_WIDTH / 2],[0,Rkns._ARROW_WIDTH]);
+    this.edge_arrow.__controller = this;
     this.edge_text = new paper.PointText();
     this.edge_text.characterStyle = {
-        fontSize: 10,
+        fontSize: Rkns._EDGE_FONT_SIZE,
         fillColor: 'black'
     };
     this.edge_text.paragraphStyle.justification = 'center';
     this.edge_text.__controller = this;
-    this.edge_angle = 0;
+    this.edge_text_angle = 0;
+    this.edge_arrow_angle = 0;
 }
 
 Rkns.Renderers.Paper__Controllers.Edge.prototype.redraw = function() {
-    this.edge_line.strokeColor = this._element.created_by.color;
     var _p0 = this.from_node_controller.node_paper_coords,
         _p1 = this.to_node_controller.node_paper_coords,
-        _a = _p1.subtract(_p0).angle;
+        _a = _p1.subtract(_p0).angle,
+        _center = _p0.add(_p1).divide(2),
+        _color = this._element.created_by.color;
+    this.edge_line.strokeColor = _color;
     this.edge_line.segments[0].point = _p0;
     this.edge_line.segments[1].point = _p1;
-    this.edge_text.content = this._element.title;
-    this.edge_text.position = _p0.add(_p1).divide(2);
+    this.edge_arrow.rotate(_a - this.edge_arrow_angle);
+    this.edge_arrow.fillColor = _color;
+    this.edge_arrow.position = _center;
+    this.edge_arrow_angle = _a;
     if (_a > 90) {
         _a -= 180;
     }
     if (_a < -90) {
         _a += 180;
     }
-    this.edge_text.rotate(_a - this.edge_angle);
-    this.edge_angle = _a;
+    this.edge_text.rotate(_a - this.edge_text_angle);
+    this.edge_text.content = this._element.title;
+    this.edge_text.position = _center;
+    this.edge_text_angle = _a;
+}
+
+Rkns.Renderers.Paper__Controllers.Edge.prototype.select = function(_delta) {
+    this.edge_line.strokeWidth = 3;
+}
+
+Rkns.Renderers.Paper__Controllers.Edge.prototype.unselect = function(_delta) {
+    this.edge_line.strokeWidth = 1;
 }
 
 Rkns.Renderers.Paper__Controllers.Edge.prototype.paperShift = function(_delta) {
@@ -89,28 +119,94 @@
     this.to_node_controller.paperShift(_delta);
     this._renderer.redraw();
 }
+/* */
+
+Rkns.Renderers.Paper__Controllers.TempEdge = Rkns.Utils.inherit(Rkns.Renderers.Paper__Controllers._Base);
+
+Rkns.Renderers.Paper__Controllers.TempEdge.prototype._init = function() {
+    this._renderer.edge_layer.activate();
+    this.type = "temp-edge";
+    var _color = this._renderer._project.current_user.color;
+    this.edge_line = new paper.Path();
+    this.edge_line.strokeColor = _color;
+    this.edge_line.add([0,0],[0,0]);
+    this.edge_line.__controller = this;
+    this.edge_arrow = new paper.Path();
+    this.edge_arrow.fillColor = _color;
+    this.edge_arrow.add([0,0],[Rkns._ARROW_LENGTH,Rkns._ARROW_WIDTH / 2],[0,Rkns._ARROW_WIDTH]);
+    this.edge_arrow.__controller = this;
+    this.edge_arrow_angle = 0;
+}
+
+Rkns.Renderers.Paper__Controllers.TempEdge.prototype.redraw = function() {
+    var _p0 = this.from_node_controller.node_paper_coords,
+        _p1 = this.end_pos,
+        _a = _p1.subtract(_p0).angle,
+        _c = _p0.add(_p1).divide(2);
+    this.edge_line.segments[0].point = _p0;
+    this.edge_line.segments[1].point = _p1;
+    this.edge_arrow.rotate(_a - this.edge_arrow_angle);
+    this.edge_arrow.position = _c;
+    this.edge_arrow_angle = _a;
+}
+
+Rkns.Renderers.Paper__Controllers.TempEdge.prototype.paperShift = function(_delta) {
+    this.end_pos = this.end_pos.add(_delta);
+    this._renderer.onMouseMove({point: this.end_pos});
+    this.redraw();
+}
+
+Rkns.Renderers.Paper__Controllers.TempEdge.prototype.finishEdge = function(_event) {
+    var _hitResult = paper.project.hitTest(_event.point);
+    if (_hitResult && typeof _hitResult.item.__controller !== "undefined") {
+        var _target = _hitResult.item.__controller;
+        if (_target.type === "node" && this.from_node_controller._element.id !== _target._element.id) {
+            this._renderer._project.addEdge({
+                from: this.from_node_controller._element.id,
+                to: _target._element.id
+            }, Rkns._RENDER_AND_SAVE)
+        }
+    }
+    this.edge_arrow.remove();
+    this.edge_line.remove();
+    this._renderer.controllers.removeId(this.id);
+}
 
 /* */
 
 Rkns.Renderers.Paper.prototype._init = function() {
-    paper.setup(document.getElementById(this._project._opts.canvas_id));
+    this._MARGIN_X = 80;
+    this._MARGIN_Y = 50;
+    var _canvas_id = this._project._opts.canvas_id;
+    this.$ = Rkns.$("#"+_canvas_id)
+    paper.setup(document.getElementById(_canvas_id));
     this.scale = 1;
     this.offset = paper.view.center;
     this.totalScroll = 0;
     this.dragging_target = null;
+    this.selected_target = null;
     this.edge_layer = new paper.Layer();
     this.node_layer = new paper.Layer();
     var _tool = new paper.Tool(),
         _this = this;
+    _tool.onMouseMove = function(_event) {
+        _this.onMouseMove(_event);
+    }
     _tool.onMouseDown = function(_event) {
         _this.onMouseDown(_event);
     }
     _tool.onMouseDrag = function(_event) {
         _this.onMouseDrag(_event);
     }
-    Rkns.$("#"+this._project._opts.canvas_id).mousewheel(function(_event, _delta) {
+    _tool.onMouseUp = function(_event) {
+        _this.onMouseUp(_event);
+    }
+    this.$.mousewheel(function(_event, _delta) {
         _this.onScroll(_event, _delta);
     })
+    this.$.dblclick(function(_event) {
+        _this.onDoubleClick(_event);
+    })
     paper.view.onResize = function(_event) {
         _this.offset = _this.offset.add(_event.delta.divide(2));
         _this.redraw();
@@ -134,32 +230,61 @@
         _miny = Math.min.apply(Math, _yy),
         _maxx = Math.max.apply(Math, _xx),
         _maxy = Math.max.apply(Math, _yy);
-    this.scale = Math.min((paper.view.size.width - 160) / (_maxx - _minx), (paper.view.size.height - 100) / (_maxy - _miny));
+    this.scale = Math.min((paper.view.size.width - 2 * this._MARGIN_X) / (_maxx - _minx), (paper.view.size.height - 2 * this._MARGIN_Y) / (_maxy - _miny));
     this.offset = paper.view.center.subtract(new paper.Point([(_maxx + _minx) / 2, (_maxy + _miny) / 2]).multiply(this.scale));
-    this.nodes = this._project.nodes.map(function(_node) {
-        return new Rkns.Renderers.Paper__Controllers.Node(_this, _node);
+    this.controllers = new Rkns.Model.List();
+    this._project.nodes.forEach(function(_node) {
+        _this.addElement("Node", _node);
     });
-    this.edges = this._project.edges.map(function(_edge) {
-        return new Rkns.Renderers.Paper__Controllers.Edge(_this, _edge);
+    this._project.edges.forEach(function(_edge) {
+        _this.addElement("Edge", _edge);
     });
     
     this.redraw();
 }
 
+Rkns.Renderers.Paper.prototype.addElement = function(_type, _element) {
+    var _el = new Rkns.Renderers.Paper__Controllers[_type](this, _element);
+    this.controllers.push(_el);
+    return _el;
+}
+
 Rkns.Renderers.Paper.prototype.redraw = function() {
-    Rkns._(this.nodes).each(function(_node) {
-        _node.redraw();
-    });
-    Rkns._(this.edges).each(function(_edge) {
-        _edge.redraw();
+    this.controllers.forEach(function(_controller) {
+        _controller.redraw();
     });
     paper.view.draw();
 }
 
+Rkns.Renderers.Paper.prototype.onMouseMove = function(_event) {
+    var _hitResult = paper.project.hitTest(_event.point);
+    if (_hitResult && typeof _hitResult.item.__controller !== "undefined") {
+        if (this.selected_target !== _hitResult.item.__controller) {
+            if (this.selected_target) {
+                this.selected_target.unselect();
+            }
+            this.selected_target = _hitResult.item.__controller;
+            this.selected_target.select();
+        }
+    } else {
+        if (this.selected_target) {
+            this.selected_target.unselect();
+        }
+        this.selected_target = null;
+    }
+}
+
 Rkns.Renderers.Paper.prototype.onMouseDown = function(_event) {
     var _hitResult = paper.project.hitTest(_event.point);
     if (_hitResult && typeof _hitResult.item.__controller !== "undefined") {
         this.dragging_target = _hitResult.item.__controller;
+        if (this.dragging_target.type === "node" && _hitResult.type === "stroke") {
+            var _tmpEdge = this.addElement("TempEdge",{});
+            _tmpEdge.end_pos = _event.point;
+            _tmpEdge.from_node_controller = this.dragging_target;
+            _tmpEdge.redraw();
+            this.dragging_target = _tmpEdge;
+        }
     } else {
         this.dragging_target = null;
     }
@@ -174,10 +299,17 @@
     }
 }
 
+Rkns.Renderers.Paper.prototype.onMouseUp = function(_event) {
+    if (this.dragging_target && this.dragging_target.type === "temp-edge") {
+        this.dragging_target.finishEdge(_event);
+    }
+    this.dragging_target = null;
+}
+
 Rkns.Renderers.Paper.prototype.onScroll = function(_event, _scrolldelta) {
     this.totalScroll += _scrolldelta;
     if (Math.abs(this.totalScroll) >= 1) {
-        var _off = Rkns.$("#"+this._project._opts.canvas_id).offset(),
+        var _off = this.$.offset(),
             _delta = new paper.Point([
                 _event.pageX - _off.left,
                 _event.pageY - _off.top
@@ -193,3 +325,22 @@
         this.redraw();
     }
 }
+
+Rkns.Renderers.Paper.prototype.onDoubleClick = function(_event) {
+    var _off = this.$.offset(),
+        _point = new paper.Point([
+            _event.pageX - _off.left,
+            _event.pageY - _off.top
+        ]);
+    var _hitResult = paper.project.hitTest(_point);
+    if (!_hitResult || typeof _hitResult.item.__controller === "undefined") {
+        var _coords = this.toModelCoords(_point);
+        this._project.addNode({
+            position: {
+                x: _coords.x,
+                y: _coords.y
+            }
+        }, Rkns._RENDER_AND_SAVE);
+    }
+    paper.view.draw();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/js/random-data.js	Fri Jul 27 19:15:32 2012 +0200
@@ -0,0 +1,49 @@
+Rkns.Serializers.RandomData = Rkns.Utils.inherit(Rkns.Serializers._Base);
+
+Rkns.Serializers.RandomData.prototype._init = function() {
+    this._USER_COUNT = 5;
+    this._NODE_COUNT = 20;
+    this._EDGE_COUNT = 40;
+    this.user_colors = ["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"];
+    this.load();
+}
+
+Rkns.Serializers.RandomData.prototype.load = function() {
+    var _p = this._project;
+    _p.title = "Random Generated Data";
+    for (var i = 0; i < this._USER_COUNT; i++) {
+        _p.users.push(new Rkns.Model.User(_p, {
+            id: "user-"+i,
+            title: "User #"+(1+i),
+            color: this.user_colors[i]
+        }));
+    }
+    for (var i = 0; i < this._NODE_COUNT; i++) {
+        _p.nodes.push(new Rkns.Model.Node(_p, {
+            id: "node-"+i,
+            title: "Node #"+(1+i),
+            created_by: "user-" + Math.floor(this._USER_COUNT*Math.random()),
+            position: {
+//                x: 200 * Math.random(),
+//                y: 150 * Math.random()
+                x: 100 * Math.cos(2 * Math.PI * i / this._NODE_COUNT),
+                y: 100 * Math.sin(2 * Math.PI * i / this._NODE_COUNT)
+            }
+        }));
+    }
+    for (var i = 0; i < this._EDGE_COUNT; i++) {
+        var _from, _to;
+        _from = _to = Math.floor(this._NODE_COUNT*Math.random());
+        while(_from === _to) {
+            _to = Math.floor(this._NODE_COUNT*Math.random());
+        }
+        _p.edges.push(new Rkns.Model.Edge(_p, {
+            id: "edge-"+i,
+            title: "Edge #"+(1+i),
+            created_by: "user-" + Math.floor(this._USER_COUNT*Math.random()),
+            from: "node-" + _from,
+            to: "node-" + _to
+        }));
+    }
+    this.handleCallbacks();
+}
--- a/client/render-test.html	Fri Jul 27 12:22:10 2012 +0200
+++ b/client/render-test.html	Fri Jul 27 19:15:32 2012 +0200
@@ -13,13 +13,19 @@
         <script src="js/main.js"></script>
         <script src="js/model.js"></script>
         <script src="js/json-serializer.js"></script>
+        <script src="js/random-data.js"></script>
         <script src="js/paper-renderer.js"></script>
         <script type="text/javascript">
             var _proj;
             $(function() {
                 _proj = new Rkns.Project({
-                    url: "data/test-data.json",
-                    canvas_id: "renkanvas"
+                    url: "data/simple-persist.php",
+                    //serializer: "RandomData",
+                    canvas_id: "renkanvas",
+                    user: {
+                        title: "anonymous",
+                        color: "#0000ff"
+                    }
                 });
             });
         </script>