diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-03-22 15:33:31 +0100 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-03-22 15:33:31 +0100 |
| commit | 7353e02e2ef4a403d4b79c1b157f2ca00b77d2fc (patch) | |
| tree | a319552e3664ea272fdfb8abf31c961bcd5e1e6e /addons/web_diagram/static/src | |
| parent | 514c63288c7d5d88d764b44696c5e3a5fc148dfb (diff) | |
| parent | 1da49a44a57aa413e5eb96afe9c6d294e09848fd (diff) | |
[MERGE] from trunk
bzr revid: xmo@openerp.com-20120322143331-iajlifomw1hohbht
Diffstat (limited to 'addons/web_diagram/static/src')
| -rw-r--r-- | addons/web_diagram/static/src/css/base_diagram.css | 68 | ||||
| -rw-r--r-- | addons/web_diagram/static/src/img/grid.jpg | bin | 4530 -> 0 bytes | |||
| -rw-r--r-- | addons/web_diagram/static/src/js/diagram.js | 352 | ||||
| -rw-r--r-- | addons/web_diagram/static/src/js/graph.js | 996 | ||||
| -rw-r--r-- | addons/web_diagram/static/src/js/vec2.js | 358 | ||||
| -rw-r--r-- | addons/web_diagram/static/src/xml/base_diagram.xml | 7 |
6 files changed, 1609 insertions, 172 deletions
diff --git a/addons/web_diagram/static/src/css/base_diagram.css b/addons/web_diagram/static/src/css/base_diagram.css index 6be8d4873d2..8d0a418f52e 100644 --- a/addons/web_diagram/static/src/css/base_diagram.css +++ b/addons/web_diagram/static/src/css/base_diagram.css @@ -1,35 +1,53 @@ +.openerp .oe_diagram_header h3.oe_diagram_title { + font-weight: normal; + color: #252424; + margin: 0 0 0 2px; +} + .openerp .oe_diagram_pager { - text-align: right; + float:right; + /*text-align: right;*/ white-space: nowrap; } .openerp .oe_diagram_buttons { float: left; } - -/*.openerp .dhx_canvas_text { - padding:30px 0 0 10px; - -webkit-transform: rotate(60deg); - -moz-transform: rotate(60deg); - -o-transform: rotate(60deg); - -ms-transform: rotate(60deg); - transform: rotate(60deg); +.openerp .clear{ + clear:both; } - -.openerp .dhx_canvas_text.dhx_axis_item_y, .openerp .dhx_canvas_text.dhx_axis_title_x { - padding: 0px; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -o-transform: rotate(0deg); - -ms-transform: rotate(0deg); - transform: rotate(0deg); +/* We use a resizable diagram-container. The problem with a + * resizable diagram is that the diagram catches the mouse events + * and the diagram is then impossible to resize. That's why the + * diagram has a height of 98.5%, so that the bottom part of the + * diagram can be used for resize + */ +.openerp .diagram-container{ + margin:0; + padding:0; + width:100%; + height:500px; + resize:vertical; + background-color:#FFF; + border:1px solid #DCDCDC; + overflow:hidden; +} +.openerp .oe_diagram_diagram{ + margin:0; + padding:0; + background-color:#FFF; + width:100%; + height:98.5%; } -.openerp .dhx_canvas_text.dhx_axis_title_y { - padding: 0; - -webkit-transform: rotate(270deg); - -moz-transform: rotate(270deg); - -o-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} */ +/* prevent accidental selection of the text in the svg nodes */ +.openerp .oe_diagram_diagram *{ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +}; + diff --git a/addons/web_diagram/static/src/img/grid.jpg b/addons/web_diagram/static/src/img/grid.jpg Binary files differdeleted file mode 100644 index 5254207b964..00000000000 --- a/addons/web_diagram/static/src/img/grid.jpg +++ /dev/null diff --git a/addons/web_diagram/static/src/js/diagram.js b/addons/web_diagram/static/src/js/diagram.js index 90f8000bc0f..41b3c4844db 100644 --- a/addons/web_diagram/static/src/js/diagram.js +++ b/addons/web_diagram/static/src/js/diagram.js @@ -55,7 +55,7 @@ openerp.web.DiagramView = openerp.web.View.extend({ this.do_update_pager(); // New Node,Edge - this.$element.find('#new_node.oe_diagram_button_new').click(function(){self.add_edit_node(null, self.node);}); + this.$element.find('#new_node.oe_diagram_button_new').click(function(){self.add_node();}); if(this.id) { self.get_diagram_info(); @@ -111,175 +111,233 @@ openerp.web.DiagramView = openerp.web.View.extend({ this.get_diagram_info(); } }, - select_node: function (node, element) { - if (!this.selected_node) { - this.selected_node = node; - element.attr('stroke', 'red'); - return; - } - // Re-click selected node, deselect it - if (node.id === this.selected_node.id) { - this.selected_node = null; - element.attr('stroke', 'black'); - return; - } - this.add_edit_node(null, this.connector, { - act_from: this.selected_node.id, - act_to: node.id - }); - }, - draw_diagram: function(result) { - this.selected_node = null; - var diagram = new Graph(); - this.active_model = result['id_model']; - - var res_nodes = result['nodes']; - var res_connectors = result['conn']; - this.parent_field = result.parent_field; - - //Custom logic + // Set-up the drawing elements of the diagram + draw_diagram: function(result) { var self = this; - var renderer = function(r, n) { - var shape = (n.node.shape === 'rectangle') ? 'rect' : 'ellipse'; + var res_nodes = result['nodes']; + var res_edges = result['conn']; + this.parent_field = result.parent_field; + this.$element.find('h3.oe_diagram_title').text(result.name); + + var id_to_node = {}; + + + var style = { + edge_color: "#A0A0A0", + edge_label_color: "#555", + edge_label_font_size: 10, + edge_width: 2, + edge_spacing: 100, + edge_loop_radius: 100, + + node_label_color: "#333", + node_label_font_size: 12, + node_outline_color: "#333", + node_outline_width: 1, + node_selected_color: "#0097BE", + node_selected_width: 2, + node_size_x: 110, + node_size_y: 80, + connector_active_color: "#FFF", + connector_radius: 4, + + close_button_radius: 8, + close_button_color: "#333", + close_button_x_color: "#FFF", + + gray: "#DCDCDC", + white: "#FFF", + + viewport_margin: 50 + }; - var node = r[shape](n.node.x, n.node.y).attr({ - "fill": n.node.color - }); + // remove previous diagram + var canvas = self.$element.find('div.oe_diagram_diagram') + .empty().get(0); - var nodes = r.set(node, r.text(n.node.x, n.node.y, (n.label || n.id))) - .attr("cursor", "pointer") - .dblclick(function() { - self.add_edit_node(n.node.id, self.node); - }) - .mousedown(function () { node.moved = false; }) - .mousemove(function () { node.moved = true; }) - .click(function () { - // Ignore click from move event - if (node.moved) { return; } - self.select_node(n.node, node); - }); - - if (shape === 'rect') { - node.attr({width: "60", height: "44"}); - node.next.attr({"text-anchor": "middle", x: n.node.x + 20, y: n.node.y + 20}); - } else { - node.attr({rx: "40", ry: "20"}); - } + var r = new Raphael(canvas, '100%','100%'); - return nodes; - }; + var graph = new CuteGraph(r,style,canvas.parentNode); + + var confirm_dialog = $('#dialog').dialog({ + autoOpen: false, + title: _t("Are you sure?") }); + - _.each(res_nodes, function(res_node) { - diagram.addNode(res_node['name'],{node: res_node,render: renderer}); + _.each(res_nodes, function(node) { + var n = new CuteNode( + graph, + node.x + 50, //FIXME the +50 should be in the layout algorithm + node.y + 50, + CuteGraph.wordwrap(node.name, 14), + node.shape === 'rectangle' ? 'rect' : 'circle', + node.color === 'white' ? style.white : style.gray); + + n.id = node.id; + id_to_node[node.id] = n; }); - // Id for Path(Edges) - var edge_ids = []; - - _.each(res_connectors, function(connector, index) { - edge_ids.push(index); - diagram.addEdge(connector['source'], connector['destination'], {directed : true, label: connector['signal']}); + _.each(res_edges, function(edge) { + var e = new CuteEdge( + graph, + CuteGraph.wordwrap(edge.signal, 32), + id_to_node[edge.s_id], + id_to_node[edge.d_id] || id_to_node[edge.s_id] ); //WORKAROUND + e.id = edge.id; }); - self.$element.find('.diagram').empty(); - - var layouter = new Graph.Layout.Ordered(diagram); - var render_diagram = new Graph.Renderer.Raphael('dia-canvas', diagram, $('div#dia-canvas').width(), $('div#dia-canvas').height()); + CuteNode.double_click_callback = function(cutenode){ + self.edit_node(cutenode.id); + }; + var i = 0; + CuteNode.destruction_callback = function(cutenode){ + if(!confirm(_t("Deleting this node cannot be undone.\nIt will also delete all connected transitions.\n\nAre you sure ?"))){ + return $.Deferred().reject().promise(); + } + return new openerp.web.DataSet(self,self.node).unlink([cutenode.id]); + }; + CuteEdge.double_click_callback = function(cuteedge){ + self.edit_connector(cuteedge.id); + }; - _.each(diagram.edges, function(edge, index) { - if(edge.connection) { - edge.connection.fg.attr({cursor: "pointer"}).dblclick(function() { - self.add_edit_node(edge_ids[index], self.connector); - }); + CuteEdge.creation_callback = function(node_start, node_end){ + return {label:_t("")}; + }; + CuteEdge.new_edge_callback = function(cuteedge){ + self.add_connector(cuteedge.get_start().id, + cuteedge.get_end().id, + cuteedge); + }; + CuteEdge.destruction_callback = function(cuteedge){ + if(!confirm(_t("Deleting this transition cannot be undone.\n\nAre you sure ?"))){ + return $.Deferred().reject().promise(); } - }); + return new openerp.web.DataSet(self,self.connector).unlink([cuteedge.id]); + }; + }, - add_edit_node: function(id, model, defaults) { - defaults = defaults || {}; + // Creates a popup to edit the content of the node with id node_id + edit_node: function(node_id){ var self = this; + var title = _t('Activity'); + var pop = new openerp.web.form.FormOpenPopup(self); - if(!model) - model = self.node; - if(id) - id = parseInt(id, 10); - - var pop, - title = model == self.node ? _t('Activity') : _t('Transition'); - if(!id) { - pop = new openerp.web.form.SelectCreatePopup(this); - pop.select_element( - model, - { - title: _t("Create:") + title, - initial_view: 'form', - disable_multiple_selection: true - }, - this.dataset.domain, - this.context || this.dataset.context - ); - pop.on_select_elements.add_last(function(element_ids) { - self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); - }); - } else { - pop = new openerp.web.form.FormOpenPopup(this); - pop.show_element( - model, - id, - this.context || this.dataset.context, + pop.show_element( + self.node, + node_id, + self.context || self.dataset.context, { title: _t("Open: ") + title } ); - pop.on_write.add(function() { - self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); + + pop.on_write.add(function() { + self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); }); - } + var form_fields = [self.parent_field]; var form_controller = pop.view_form; - var form_fields; - - if (model === self.node) { - form_fields = [this.parent_field]; - if (!id) { - form_controller.on_record_loaded.add_last(function() { - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; - field.set_value([self.id,self.active_model]); - field.dirty = true; - }); - }); - } else { - form_controller.on_record_loaded.add_first(function() { - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; - field.$input.prop('disabled', true); - field.$drop_down.unbind(); - field.$menu_btn.unbind(); - }); - }); - } - } else { - form_fields = [ - this.connectors.attrs.source, - this.connectors.attrs.destination]; - } + form_controller.on_record_loaded.add_first(function() { + _.each(form_fields, function(fld) { + if (!(fld in form_controller.fields)) { return; } + var field = form_controller.fields[fld]; + field.$input.prop('disabled', true); + field.$drop_down.unbind(); + field.$menu_btn.unbind(); + }); + }); + }, + + // Creates a popup to add a node to the diagram + add_node: function(){ + var self = this; + var title = _t('Activity'); + var pop = new openerp.web.form.SelectCreatePopup(self); + pop.select_element( + self.node, + { + title: _t("Create:") + title, + initial_view: 'form', + disable_multiple_selection: true + }, + self.dataset.domain, + self.context || self.dataset.context + ); + pop.on_select_elements.add_last(function(element_ids) { + self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); + }); - if (!_.isEmpty(defaults)) { - form_controller.on_record_loaded.add_last(function () { - _(form_fields).each(function (field) { - if (!defaults[field]) { return; } - form_controller.fields[field].set_value(defaults[field]); - form_controller.fields[field].dirty = true; - }); + var form_controller = pop.view_form; + var form_fields = [this.parent_field]; + + form_controller.on_record_loaded.add_last(function() { + _.each(form_fields, function(fld) { + if (!(fld in form_controller.fields)) { return; } + var field = form_controller.fields[fld]; + field.set_value(self.id); + field.dirty = true; }); - } - - + }); + }, + + // Creates a popup to edit the connector of id connector_id + edit_connector: function(connector_id){ + var self = this; + var title = _t('Transition'); + var pop = new openerp.web.form.FormOpenPopup(self); + pop.show_element( + self.connector, + parseInt(connector_id,10), //FIXME Isn't connector_id supposed to be an int ? + self.context || self.dataset.context, + { + title: _t("Open: ") + title + } + ); + pop.on_write.add(function() { + self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); + }); + }, + + // Creates a popup to add a connector from node_source_id to node_dest_id. + // dummy_cuteedge if not null, will be removed form the graph after the popup is closed. + add_connector: function(node_source_id, node_dest_id, dummy_cuteedge){ + var self = this; + var title = _t('Transition'); + var pop = new openerp.web.form.SelectCreatePopup(self); + + pop.select_element( + self.connector, + { + title: _t("Create:") + title, + initial_view: 'form', + disable_multiple_selection: true + }, + this.dataset.domain, + this.context || this.dataset.context + ); + + pop.on_select_elements.add_last(function(element_ids) { + self.dataset.read_index(_.keys(self.fields_view.fields)).pipe(self.on_diagram_loaded); + }); + // We want to destroy the dummy edge after a creation cancel. This destroys it even if we save the changes. + // This is not a problem since the diagram is completely redrawn on saved changes. + pop.$element.bind("dialogbeforeclose",function(){ + if(dummy_cuteedge){ + dummy_cuteedge.remove(); + } + }); + + var form_controller = pop.view_form; + + form_controller.on_record_loaded.add_last(function () { + form_controller.fields[self.connectors.attrs.source].set_value(node_source_id); + form_controller.fields[self.connectors.attrs.source].dirty = true; + form_controller.fields[self.connectors.attrs.destination].set_value(node_dest_id); + form_controller.fields[self.connectors.attrs.destination].dirty = true; + }); }, on_pager_action: function(action) { @@ -297,8 +355,10 @@ openerp.web.DiagramView = openerp.web.View.extend({ this.dataset.index = this.dataset.ids.length - 1; break; } - this.dataset.read_index(_.keys(this.fields_view.fields)).pipe(this.on_diagram_loaded); + var loaded = this.dataset.read_index(_.keys(this.fields_view.fields)) + .pipe(this.on_diagram_loaded); this.do_update_pager(); + return loaded; }, do_update_pager: function(hide_index) { @@ -313,7 +373,7 @@ openerp.web.DiagramView = openerp.web.View.extend({ do_show: function() { this.do_push_state({}); - return this._super(); + return $.when(this._super(), this.on_pager_action('reload')); } }); }; diff --git a/addons/web_diagram/static/src/js/graph.js b/addons/web_diagram/static/src/js/graph.js new file mode 100644 index 00000000000..b61b2bf7a6b --- /dev/null +++ b/addons/web_diagram/static/src/js/graph.js @@ -0,0 +1,996 @@ + +(function(window){ + + // this serves as the end of an edge when creating a link + function EdgeEnd(pos_x,pos_y){ + this.x = pos_x; + this.y = pos_y; + + this.get_pos = function(){ + return new Vec2(this.x,this.y); + } + } + + // A close button, + // if entity_type == "node": + // GraphNode.destruction_callback(entity) is called where entity is a node. + // If it returns true the node and all connected edges are destroyed. + // if entity_type == "edge": + // GraphEdge.destruction_callback(entity) is called where entity is an edge + // If it returns true the edge is destroyed + // pos_x,pos_y is the relative position of the close button to the entity position (entity.get_pos()) + + function CloseButton(graph, entity, entity_type, pos_x,pos_y){ + var self = this; + var visible = false; + var close_button_radius = graph.style.close_button_radius || 8; + var close_circle = graph.r.circle( entity.get_pos().x + pos_x, + entity.get_pos().y + pos_y, + close_button_radius ); + //the outer gray circle + close_circle.attr({ 'opacity': 0, + 'fill': graph.style.close_button_color || "black", + 'cursor': 'pointer', + 'stroke': 'none' }); + close_circle.transform(graph.get_transform()); + graph.set_scrolling(close_circle); + + //the 'x' inside the circle + var close_label = graph.r.text( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y,"x"); + close_label.attr({ 'fill': graph.style.close_button_x_color || "white", + 'font-size': close_button_radius, + 'cursor': 'pointer' }); + + close_label.transform(graph.get_transform()); + graph.set_scrolling(close_label); + + // the dummy_circle is used to catch events, and avoid hover in/out madness + // between the 'x' and the button + var dummy_circle = graph.r.circle( entity.get_pos().x + pos_x, + entity.get_pos().y + pos_y, + close_button_radius ); + dummy_circle.attr({'opacity':1, 'fill': 'transparent', 'stroke':'none', 'cursor':'pointer'}); + dummy_circle.transform(graph.get_transform()); + graph.set_scrolling(dummy_circle); + + this.get_pos = function(){ + return entity.get_pos().add_xy(pos_x,pos_y); + }; + + this.update_pos = function(){ + var pos = self.get_pos(); + close_circle.attr({'cx':pos.x, 'cy':pos.y}); + dummy_circle.attr({'cx':pos.x, 'cy':pos.y}); + close_label.attr({'x':pos.x, 'y':pos.y}); + }; + + function hover_in(){ + if(!visible){ return; } + close_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic'); + dummy_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic'); + } + function hover_out(){ + if(!visible){ return; } + close_circle.animate({'r': close_button_radius},400,'linear'); + dummy_circle.animate({'r': close_button_radius},400,'linear'); + } + dummy_circle.hover(hover_in,hover_out); + + function click_action(){ + if(!visible){ return; } + + close_circle.attr({'r': close_button_radius * 2 }); + dummy_circle.attr({'r': close_button_radius * 2 }); + close_circle.animate({'r': close_button_radius }, 400, 'linear'); + dummy_circle.animate({'r': close_button_radius }, 400, 'linear'); + + if(entity_type == "node"){ + $.when(GraphNode.destruction_callback(entity)).then(function () { + //console.log("remove node",entity); + entity.remove(); + }); + }else if(entity_type == "edge"){ + $.when(GraphEdge.destruction_callback(entity)).then(function () { + //console.log("remove edge",entity); + entity.remove(); + }); + } + } + dummy_circle.click(click_action); + + this.show = function(){ + if(!visible){ + close_circle.animate({'opacity':1}, 100, 'linear'); + close_label.animate({'opacity':1}, 100, 'linear'); + visible = true; + } + } + this.hide = function(){ + if(visible){ + close_circle.animate({'opacity':0}, 100, 'linear'); + close_label.animate({'opacity':0}, 100, 'linear'); + visible = false; + } + } + //destroy this object and remove it from the graph + this.remove = function(){ + if(visible){ + visible = false; + close_circle.animate({'opacity':0}, 100, 'linear'); + close_label.animate({'opacity':0}, 100, 'linear',self.remove); + }else{ + close_circle.remove(); + close_label.remove(); + dummy_circle.remove(); + } + } + } + + // connectors are start and end point of edge creation drags. + function Connector(graph,node,pos_x,pos_y){ + var visible = false; + var conn_circle = graph.r.circle(node.get_pos().x + pos_x, node.get_pos().y + pos_y,4); + conn_circle.attr({ 'opacity': 0, + 'fill': graph.style.node_outline_color, + 'stroke': 'none' }); + conn_circle.transform(graph.get_transform()); + graph.set_scrolling(conn_circle); + + var self = this; + + this.update_pos = function(){ + conn_circle.attr({'cx':node.get_pos().x + pos_x, 'cy':node.get_pos().y + pos_y}); + }; + this.get_pos = function(){ + return new node.get_pos().add_xy(pos_x,pos_y); + }; + this.remove = function(){ + conn_circle.remove(); + } + function hover_in(){ + if(!visible){ return;} + conn_circle.animate({'r':8},300,'elastic'); + if(graph.creating_edge){ + graph.target_node = node; + conn_circle.animate({ 'fill': graph.style.connector_active_color, + 'stroke': graph.style.node_outline_color, + 'stroke-width': graph.style.node_selected_width, + },100,'linear'); + } + } + function hover_out(){ + if(!visible){ return;} + conn_circle.animate({ 'r':graph.style.connector_radius, + 'fill':graph.style.node_outline_color, + 'stroke':'none'},400,'linear'); + graph.target_node = null; + } + conn_circle.hover(hover_in,hover_out); + + + var drag_down = function(){ + if(!visible){ return; } + self.ox = conn_circle.attr("cx"); + self.oy = conn_circle.attr("cy"); + self.edge_start = new EdgeEnd(self.ox,self.oy); + self.edge_end = new EdgeEnd(self.ox, self.oy); + self.edge_tmp = new GraphEdge(graph,'',self.edge_start,self.edge_end,true); + graph.creating_edge = true; + }; + var drag_move = function(dx,dy){ + if(!visible){ return; } + self.edge_end.x = self.ox + dx; + self.edge_end.y = self.oy + dy; + self.edge_tmp.update(); + }; + var drag_up = function(){ + if(!visible){ return; } + graph.creating_edge = false; + self.edge_tmp.remove(); + if(graph.target_node){ + var edge_prop = GraphEdge.creation_callback(node,graph.target_node); + if(edge_prop){ + var new_edge = new GraphEdge(graph,edge_prop.label, node,graph.target_node); + GraphEdge.new_edge_callback(new_edge); + } + } + }; + conn_circle.drag(drag_move,drag_down,drag_up); + + function show(){ + if(!visible){ + conn_circle.animate({'opacity':1}, 100, 'linear'); + visible = true; + } + } + function hide(){ + if(visible){ + conn_circle.animate({'opacity':0}, 100, 'linear'); + visible = false; + } + } + this.show = show; + this.hide = hide; + } + + //Creates a new graph on raphael document r. + //style is a dictionary containing the style definitions + //viewport (optional) is the dom element representing the viewport of the graph. It is used + //to prevent scrolling to scroll the graph outside the viewport. + + function Graph(r,style,viewport){ + var self = this; + var nodes = []; // list of all nodes in the graph + var edges = []; // list of all edges in the graph + var graph = {}; // graph[n1.uid][n2.uid] -> list of all edges from n1 to n2 + var links = {}; // links[n.uid] -> list of all edges from or to n + var uid = 1; // all nodes and edges have an uid used to order their display when they are curved + var selected_entity = null; //the selected entity (node or edge) + + self.creating_edge = false; // true if we are dragging a new edge onto a node + self.target_node = null; // this holds the target node when creating an edge and hovering a connector + self.r = r; // the raphael instance + self.style = style; // definition of the colors, spacing, fonts, ... used by the elements + var tr_x = 0, tr_y = 0; // global translation coordinate + + var background = r.rect(0,0,'100%','100%').attr({'fill':'white', 'stroke':'none', 'opacity':0, 'cursor':'move'}); + + // return the global transform of the scene + this.get_transform = function(){ + return "T"+tr_x+","+tr_y + }; + + + // translate every element of the graph except the background. + // elements inserted in the graph after a translate_all() must manually apply transformation + // via get_transform() + var translate_all = function(dx,dy){ + tr_x += dx; + tr_y += dy; + var tstr = self.get_transform(); + + r.forEach(function(el){ + if(el != background){ + el.transform(tstr); + } + }); + }; + //returns {minx, miny, maxx, maxy}, the translated bounds containing all nodes + var get_bounds = function(){ + var minx = Number.MAX_VALUE; + var miny = Number.MAX_VALUE; + var maxx = Number.MIN_VALUE; + var maxy = Number.MIN_VALUE; + + for(var i = 0; i < nodes.length; i++){ + var pos = nodes[i].get_pos(); + minx = Math.min(minx,pos.x); + miny = Math.min(miny,pos.y); + maxx = Math.max(maxx,pos.x); + maxy = Math.max(maxy,pos.y); + } + + minx = minx - style.node_size_x / 2 + tr_x; + miny = miny - style.node_size_y / 2 + tr_y; + maxx = maxx + style.node_size_x / 2 + tr_x; + maxy = maxy + style.node_size_y / 2 + tr_y; + + return { minx:minx, miny:miny, maxx:maxx, maxy:maxy }; + + }; + // returns false if the translation dx,dy of the viewport + // hides the graph (with optional margin) + var translation_respects_viewport = function(dx,dy,margin){ + if(!viewport){ + return true; + } + margin = margin || 0; + var b = get_bounds(); + var width = viewport.offsetWidth; + var height = viewport.offsetHeight; + + if( ( dy < 0 && b.maxy + dy < margin ) || + ( dy > 0 && b.miny + dy > height - margin ) || + ( dx < 0 && b.maxx + dx < margin ) || + ( dx > 0 && b.minx + dx > width - margin ) ){ + return false; + } + + return true; + } + //Adds a mousewheel event callback to raph_element that scrolls the viewport + this.set_scrolling = function(raph_element){ + $(raph_element.node).bind('mousewheel',function(event,delta){ + var dy = delta * 20; + if( translation_respects_viewport(0,dy, style.viewport_margin) ){ + translate_all(0,dy); + } + }); + }; + + var px, py; + // Graph translation when background is dragged + var bg_drag_down = function(){ + px = py = 0; + }; + var bg_drag_move = function(x,y){ + var dx = x - px; + var dy = y - py; + px = x; + py = y; + if( translation_respects_viewport(dx,dy, style.viewport_margin) ){ + translate_all(dx,dy); + } + }; + var bg_drag_up = function(){}; + background.drag( bg_drag_move, bg_drag_down, bg_drag_up); + + this.set_scrolling(background); + + //adds a node to the graph and sets its uid. + this.add_node = function (n){ + nodes.push(n); + n.uid = uid++; + }; + + //return the list of all nodes in the graph + this.get_node_list = function(){ + return nodes; + }; + + //adds an edge to the graph and sets its uid + this.add_edge = function (n1,n2,e){ + edges.push(e); + e.uid = uid++; + if(!graph[n1.uid]) graph[n1.uid] = {}; + if(!graph[n1.uid][n2.uid]) graph[n1.uid][n2.uid] = []; + if(!links[n1.uid]) links[n1.uid] = []; + if(!links[n2.uid]) links[n2.uid] = []; + + graph[n1.uid][n2.uid].push(e); + links[n1.uid].push(e); + if(n1 != n2){ + links[n2.uid].push(e); + } + }; + + //removes an edge from the graph + this.remove_edge = function(edge){ + edges = _.without(edges,edge); + var n1 = edge.get_start(); + var n2 = edge.get_end(); + links[n1.uid] = _.without(links[n1.uid],edge); + links[n2.uid] = _.without(links[n2.uid],edge); + graph[n1.uid][n2.uid] = _.without(graph[n1.uid][n2.uid],edge); + if ( selected_entity == edge ){ + selected_entity = null; + } + }; + //removes a node and all connected edges from the graph + this.remove_node = function(node){ + var linked_edges = self.get_linked_edge_list(node); + for(var i = 0; i < linked_edges.length; i++){ + linked_edges[i].remove(); + } + nodes = _.without(nodes,node); + + if ( selected_entity == node ){ + selected_entity = null; + } + } + + + //return the list of edges from n1 to n2 + this.get_edge_list = function(n1,n2){ + var list = []; + if(!graph[n1.uid]) return list; + if(!graph[n1.uid][n2.uid]) return list; + return graph[n1.uid][n2.uid]; + }; + //returns the list of all edge connected to n + this.get_linked_edge_list = function(n){ + if(!links[n.uid]) return []; + return links[n.uid]; + }; + //return a curvature index so that all edges connecting n1,n2 have different curvatures + this.get_edge_curvature = function(n1,n2,e){ + var el_12 = this.get_edge_list(n1,n2); + var c12 = el_12.length; + var el_21 = this.get_edge_list(n2,n1); + var c21 = el_21.length; + if(c12 + c21 == 1){ // only one edge + return 0; + }else{ + var index = 0; + for(var i = 0; i < c12; i++){ + if (el_12[i].uid < e.uid){ + index++; + } + } + if(c21 == 0){ // all edges in the same direction + return index - (c12-1)/2.0; + }else{ + return index + 0.5; + } + } + }; + + + // Returns the angle in degrees of the edge loop. We do not support more than 8 loops on one node + this.get_loop_angle = function(n,e){ + var loop_list = this.get_edge_list(n,n); + + var slots = []; // the 8 angles where we can put the loops + for(var angle = 0; angle < 360; angle += 45){ + slots.push(Vec2.new_polar_deg(1,angle)); + } + + //we assign to each slot a score. The higher the score, the closer it is to other edges. + var links = this.get_linked_edge_list(n); + for(var i = 0; i < links.length; i++){ + var edge = links[i]; + if(!edge.is_loop || edge.is_loop()){ + continue; + } + var end = edge.get_end(); + if (end == n){ + end = edge.get_start(); + } + var dir = end.get_pos().sub(n.get_pos()).normalize(); + for(var s = 0; s < slots.length; s++){ + var score = slots[s].dot(dir); + if(score < 0){ + score = -0.2*Math.pow(score,2); + }else{ + score = Math.pow(score,2); + } + if(!slots[s].score){ + slots[s].score = score; + }else{ + slots[s].score += score; + } + } + } + //we want the loops with lower uid to get the slots with the lower score + slots.sort(function(a,b){ return a.score < b.score ? -1: 1; }); + + var index = 0; + for(var i = 0; i < links.length; i++){ + var edge = links[i]; + if(!edge.is_loop || !edge.is_loop()){ + continue; + } + if(edge.uid < e.uid){ + index++; + } + } + index %= slots.length; + + return slots[index].angle_deg(); + } + + //selects a node or an edge and deselects everything else + this.select = function(entity){ + if(selected_entity){ + if(selected_entity == entity){ + return; + }else{ + if(selected_entity.set_not_selected){ + selected_entity.set_not_selected(); + } + selected_entity = null; + } + } + selected_entity = entity; + if(entity && entity.set_selected){ + entity.set_selected(); + } + }; + } + + // creates a new Graph Node on Raphael document r, centered on [pos_x,pos_y], with label 'label', + // and of type 'circle' or 'rect', and of color 'color' + function GraphNode(graph,pos_x, pos_y,label,type,color){ + var self = this; + var r = graph.r; + var sy = graph.style.node_size_y; + var sx = graph.style.node_size_x; + var node_fig = null; + var selected = false; + this.connectors = []; + this.close_button = null; + this.uid = 0; + + graph.add_node(this); + + if(type == 'circle'){ + node_fig = r.ellipse(pos_x,pos_y,sx/2,sy/2); + }else{ + node_fig = r.rect(pos_x-sx/2,pos_y-sy/2,sx,sy); + } + node_fig.attr({ 'fill': color, + 'stroke': graph.style.node_outline_color, + 'stroke-width': graph.style.node_outline_width, + 'cursor':'pointer' }); + node_fig.transform(graph.get_transform()); + graph.set_scrolling(node_fig); + + var node_label = r.text(pos_x,pos_y,label); + node_label.attr({ 'fill': graph.style.node_label_color, + 'font-size': graph.style.node_label_font_size, + 'cursor': 'pointer' }); + node_label.transform(graph.get_transform()); + graph.set_scrolling(node_label); + + // redraws all edges linked to this node + var update_linked_edges = function(){ + var edges = graph.get_linked_edge_list(self); + for(var i = 0; i < edges.length; i++){ + edges[i].update(); + } + }; + + // sets the center position of the node + var set_pos = function(pos){ + if(type == 'circle'){ + node_fig.attr({'cx':pos.x,'cy':pos.y}); + }else{ + node_fig.attr({'x':pos.x-sx/2,'y':pos.y-sy/2}); + } + node_label.attr({'x':pos.x,'y':pos.y}); + for(var i = 0; i < self.connectors.length; i++){ + self.connectors[i].update_pos(); + } + if(self.close_button){ + self.close_button.update_pos(); + } + update_linked_edges(); + }; + // returns the figure used to draw the node + var get_fig = function(){ + return node_fig; + }; + // returns the center coordinates + var get_pos = function(){ + if(type == 'circle'){ + return new Vec2(node_fig.attr('cx'), node_fig.attr('cy')); + }else{ + return new Vec2(node_fig.attr('x') + sx/2, node_fig.attr('y') + sy/2); + } + }; + // return the label string + var get_label = function(){ + return node_label.attr("text"); + }; + // sets the label string + var set_label = function(text){ + node_label.attr({'text':text}); + }; + var get_bound = function(){ + if(type == 'circle'){ + return new BEllipse(get_pos().x,get_pos().y,sx/2,sy/2); + }else{ + return BRect.new_centered(get_pos().x,get_pos().y,sx,sy); + } + }; + // selects this node and deselects all other nodes + var set_selected = function(){ + if(!selected){ + selected = true; + node_fig.attr({ 'stroke': graph.style.node_selected_color, + 'stroke-width': graph.style.node_selected_width }); + if(!self.close_button){ + self.close_button = new CloseButton(graph,self, "node" ,sx/2 , - sy/2); + self.close_button.show(); + } + for(var i = 0; i < self.connectors.length; i++){ + self.connectors[i].show(); + } + } + }; + // deselect this node + var set_not_selected = function(){ + if(selected){ + node_fig.animate({ 'stroke': graph.style.node_outline_color, + 'stroke-width': graph.style.node_outline_width }, + 100,'linear'); + if(self.close_button){ + self.close_button.remove(); + self.close_button = null; + } + selected = false; + } + for(var i = 0; i < self.connectors.length; i++){ + self.connectors[i].hide(); + } + }; + var remove = function(){ + if(self.close_button){ + self.close_button.remove(); + } + for(var i = 0; i < self.connectors.length; i++){ + self.connectors[i].remove(); + } + graph.remove_node(self); + node_fig.remove(); + node_label.remove(); + } + + + this.set_pos = set_pos; + this.get_pos = get_pos; + this.set_label = set_label; + this.get_label = get_label; + this.get_bound = get_bound; + this.get_fig = get_fig; + this.set_selected = set_selected; + this.set_not_selected = set_not_selected; + this.update_linked_edges = update_linked_edges; + this.remove = remove; + + + //select the node and play an animation when clicked + var click_action = function(){ + if(type == 'circle'){ + node_fig.attr({'rx':sx/2 + 3, 'ry':sy/2+ 3}); + node_fig.animate({'rx':sx/2, 'ry':sy/2},500,'elastic'); + }else{ + var cx = get_pos().x; + var cy = get_pos().y; + node_fig.attr({'x':cx - (sx/2) - 3, 'y':cy - (sy/2) - 3, 'ẃidth':sx+6, 'height':sy+6}); + node_fig.animate({'x':cx - sx/2, 'y':cy - sy/2, 'ẃidth':sx, 'height':sy},500,'elastic'); + } + graph.select(self); + }; + node_fig.click(click_action); + node_label.click(click_action); + + //move the node when dragged + var drag_down = function(){ + this.opos = get_pos(); + }; + var drag_move = function(dx,dy){ + // we disable labels when moving for performance reasons, + // updating the label position is quite expensive + // we put this here because drag_down is also called on simple clicks ... and this causes unwanted flicker + var edges = graph.get_linked_edge_list(self); + for(var i = 0; i < edges.length; i++){ + edges[i].label_disable(); + } + if(self.close_button){ + self.close_button.hide(); + } + set_pos(this.opos.add_xy(dx,dy)); + }; + var drag_up = function(){ + //we re-enable the + var edges = graph.get_linked_edge_list(self); + for(var i = 0; i < edges.length; i++){ + edges[i].label_enable(); + } + if(self.close_button){ + self.close_button.show(); + } + }; + node_fig.drag(drag_move,drag_down,drag_up); + node_label.drag(drag_move,drag_down,drag_up); + + //allow the user to create edges by dragging onto the node + function hover_in(){ + if(graph.creating_edge){ + graph.target_node = self; + } + } + function hover_out(){ + graph.target_node = null; + } + node_fig.hover(hover_in,hover_out); + node_label.hover(hover_in,hover_out); + + function double_click(){ + GraphNode.double_click_callback(self); + } + node_fig.dblclick(double_click); + node_label.dblclick(double_click); + + this.connectors.push(new Connector(graph,this,-sx/2,0)); + this.connectors.push(new Connector(graph,this,sx/2,0)); + this.connectors.push(new Connector(graph,this,0,-sy/2)); + this.connectors.push(new Connector(graph,this,0,sy/2)); + + this.close_button = new CloseButton(graph,this,"node",sx/2 , - sy/2 ); + } + + GraphNode.double_click_callback = function(node){ + console.log("double click from node:",node); + }; + + // this is the default node destruction callback. It is called before the node is removed from the graph + // and before the connected edges are destroyed + GraphNode.destruction_callback = function(node){ return true; }; + + // creates a new edge with label 'label' from start to end. start and end must implement get_pos_*, + // if tmp is true, the edge is not added to the graph, used for drag edges. + // replace tmp == false by graph == null + function GraphEdge(graph,label,start,end,tmp){ + var self = this; + var r = graph.r; + var curvature = 0; // 0 = straight, != 0 curved + var s,e; // positions of the start and end point of the line between start and end + var mc; // position of the middle of the curve (bezier control point) + var mc1,mc2; // control points of the cubic bezier for the loop edges + var elfs = graph.style.edge_label_font_size || 10 ; + var label_enabled = true; + this.uid = 0; // unique id used to order the curved edges + var edge_path = ""; // svg definition of the edge vector path + var selected = false; + + if(!tmp){ + graph.add_edge(start,end,this); + } + + //Return the position of the label + function get_label_pos(path){ + var cpos = path.getTotalLength() * 0.5; + var cindex = Math.abs(Math.floor(curvature)); + var mod = ((cindex % 3)) * (elfs * 3.1) - (elfs * 0.5); + var verticality = Math.abs(end.get_pos().sub(start.get_pos()).normalize().dot_xy(0,1)); + verticality = Math.max(verticality-0.5,0)*2; + + var lpos = path.getPointAtLength(cpos + mod * verticality); + return new Vec2(lpos.x,lpos.y - elfs *(1-verticality)); + } + + //used by close_button + this.get_pos = function(){ + if(!edge){ + return start.get_pos().lerp(end.get_pos(),0.5); + } + return get_label_pos(edge); + /* + var bbox = edge_label.getBBox(); Does not work... :( + return new Vec2(bbox.x + bbox.width, bbox.y);*/ + } + + //Straight line from s to e + function make_line(){ + return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y ; + } + //Curved line from s to e by mc + function make_curve(){ + return "M" + s.x + "," + s.y + "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y; + } + //Curved line from s to e by mc1 mc2 + function make_loop(){ + return "M" + s.x + " " + s.y + + "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " + mc2.y + " " + e.x + " " + e.y; + } + + //computes new start and end line coordinates + function update_curve(){ + if(start != end){ + if(!tmp){ + curvature = graph.get_edge_curvature(start,end,self); + }else{ + curvature = 0; + } + s = start.get_pos(); + e = end.get_pos(); + + mc = s.lerp(e,0.5); //middle of the line s->e + var se = e.sub(s); + se = se.normalize(); + se = se.rotate_deg(-90); + se = se.scale(curvature * graph.style.edge_spacing); + mc = mc.add(se); + + if(start.get_bound){ + var col = start.get_bound().collide_segment(s,mc); + if(col.length > 0){ + s = col[0]; + } + } + if(end.get_bound){ + var col = end.get_bound().collide_segment(mc,e); + if(col.length > 0){ + e = col[0]; + } + } + + if(curvature != 0){ + edge_path = make_curve(); + }else{ + edge_path = make_line(); + } + }else{ // start == end + var rad = graph.style.edge_loop_radius || 100; + s = start.get_pos(); + e = end.get_pos(); + + var r = Vec2.new_polar_deg(rad,graph.get_loop_angle(start,self)); + mc = s.add(r); + var p = r.rotate_deg(90); + mc1 = mc.add(p.set_len(rad*0.5)); + mc2 = mc.add(p.set_len(-rad*0.5)); + + if(start.get_bound){ + var col = start.get_bound().collide_segment(s,mc1); + if(col.length > 0){ + s = col[0]; + } + var col = start.get_bound().collide_segment(e,mc2); + if(col.length > 0){ + e = col[0]; + } + } + edge_path = make_loop(); + } + } + + update_curve(); + var edge = r.path(edge_path).attr({ 'stroke': graph.style.edge_color, + 'stroke-width': graph.style.edge_width, + 'arrow-end': 'block-wide-long', + 'cursor':'pointer' }).insertBefore(graph.get_node_list()[0].get_fig()); + var labelpos = get_label_pos(edge); + var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr({ + 'fill': graph.style.edge_label_color, + 'cursor': 'pointer', + 'font-size': elfs }); + + edge.transform(graph.get_transform()); + graph.set_scrolling(edge); + + edge_label.transform(graph.get_transform()); + graph.set_scrolling(edge_label); + + + //since we create an edge we need to recompute the edges that have the same start and end positions as this one + if(!tmp){ + var edges_start = graph.get_linked_edge_list(start); + var edges_end = graph.get_linked_edge_list(end); + var edges = edges_start.length < edges_end.length ? edges_start : edges_end; + for(var i = 0; i < edges.length; i ++){ + if(edges[i] != self){ + edges[i].update(); + } + } + } + function label_enable(){ + if(!label_enabled){ + label_enabled = true; + edge_label.animate({'opacity':1},100,'linear'); + if(self.close_button){ + self.close_button.show(); + } + self.update(); + } + } + function label_disable(){ + if(label_enabled){ + label_enabled = false; + edge_label.animate({'opacity':0},100,'linear'); + if(self.close_button){ + self.close_button.hide(); + } + } + } + //update the positions + function update(){ + update_curve(); + edge.attr({'path':edge_path}); + if(label_enabled){ + var labelpos = get_label_pos(edge); + edge_label.attr({'x':labelpos.x, 'y':labelpos.y - 14}); + } + } + // removes the edge from the scene, disconnects it from linked + // nodes, destroy its drawable elements. + function remove(){ + edge.remove(); + edge_label.remove(); + if(!tmp){ + graph.remove_edge(self); + } + if(start.update_linked_edges){ + start.update_linked_edges(); + } + if(start != end && end.update_linked_edges){ + end.update_linked_edges(); + } + if(self.close_button){ + self.close_button.remove(); + } + } + + this.set_selected = function(){ + if(!selected){ + selected = true; + edge.attr({ 'stroke': graph.style.node_selected_color, + 'stroke-width': graph.style.node_selected_width }); + edge_label.attr({ 'fill': graph.style.node_selected_color }); + if(!self.close_button){ + self.close_button = new CloseButton(graph,self,"edge",0,30); + self.close_button.show(); + } + } + }; + + this.set_not_selected = function(){ + if(selected){ + selected = false; + edge.animate({ 'stroke': graph.style.edge_color, + 'stroke-width': graph.style.edge_width }, 100,'linear'); + edge_label.animate({ 'fill': graph.style.edge_label_color}, 100, 'linear'); + if(self.close_button){ + self.close_button.remove(); + self.close_button = null; + } + } + }; + function click_action(){ + graph.select(self); + } + edge.click(click_action); + edge_label.click(click_action); + + function double_click_action(){ + GraphEdge.double_click_callback(self); + } + + edge.dblclick(double_click_action); + edge_label.dblclick(double_click_action); + + + this.label_enable = label_enable; + this.label_disable = label_disable; + this.update = update; + this.remove = remove; + this.is_loop = function(){ return start == end; }; + this.get_start = function(){ return start; }; + this.get_end = function(){ return end; }; + } + + GraphEdge.double_click_callback = function(edge){ + console.log("double click from edge:",edge); + }; + + // this is the default edge creation callback. It is called before an edge is created + // It returns an object containing the properties of the edge. + // If it returns null, the edge is not created. + GraphEdge.creation_callback = function(start,end){ + var edge_prop = {}; + edge_prop.label = 'new edge!'; + return edge_prop; + }; + // This is is called after a new edge is created, with the new edge + // as parameter + GraphEdge.new_edge_callback = function(new_edge){}; + + // this is the default edge destruction callback. It is called before + // an edge is removed from the graph. + GraphEdge.destruction_callback = function(edge){ return true; }; + + + + // returns a new string with the same content as str, but with lines of maximum 'width' characters. + // lines are broken on words, or into words if a word is longer than 'width' + function wordwrap( str, width) { + // http://james.padolsey.com/javascript/wordwrap-for-javascript/ + width = width || 32; + var cut = true; + var brk = '\n'; + if (!str) { return str; } + var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)'); + return str.match(new RegExp(regex, 'g') ).join( brk ); + } + + window.CuteGraph = Graph; + window.CuteNode = GraphNode; + window.CuteEdge = GraphEdge; + + window.CuteGraph.wordwrap = wordwrap; + + +})(window); + diff --git a/addons/web_diagram/static/src/js/vec2.js b/addons/web_diagram/static/src/js/vec2.js new file mode 100644 index 00000000000..28790008be3 --- /dev/null +++ b/addons/web_diagram/static/src/js/vec2.js @@ -0,0 +1,358 @@ + +(function(window){ + + // A Javascript 2D vector library + // conventions : + // method that returns a float value do not modify the vector + // method that implement operators return a new vector with the modifications without + // modifying the calling vector or the parameters. + // + // v3 = v1.add(v2); // v3 is set to v1 + v2, v1, v2 are not modified + // + // methods that take a single vector as a parameter are usually also available with + // q '_xy' suffix. Those method takes two floats representing the x,y coordinates of + // the vector parameter and allow you to avoid to needlessly create a vector object : + // + // v2 = v1.add(new Vec2(3,4)); + // v2 = v1.add_xy(3,4); //equivalent to previous line + // + // angles are in radians by default but method that takes angle as parameters + // or return angle values usually have a variant with a '_deg' suffix that works in degrees + // + + // The 2D vector object + function Vec2(x,y){ + this.x = x; + this.y = y; + } + + window.Vec2 = Vec2; + + // Multiply a number expressed in radiant by rad2deg to convert it in degrees + var rad2deg = 57.29577951308232; + // Multiply a number expressed in degrees by deg2rad to convert it to radiant + var deg2rad = 0.017453292519943295; + // The numerical precision used to compare vector equality + var epsilon = 0.0000001; + + // This static method creates a new vector from polar coordinates with the angle expressed + // in degrees + Vec2.new_polar_deg = function(len,angle){ + var v = new Vec2(len,0); + return v.rotate_deg(angle); + }; + // This static method creates a new vector from polar coordinates with the angle expressed in + // radians + Vec2.new_polar = function(len,angle){ + var v = new Vec2(len,0); + v.rotate(angle); + return v; + }; + // returns the length or modulus or magnitude of the vector + Vec2.prototype.len = function(){ + return Math.sqrt(this.x*this.x + this.y*this.y); + }; + // returns the squared length of the vector, this method is much faster than len() + Vec2.prototype.len_sq = function(){ + return this.x*this.x + this.y*this.y; + }; + // return the distance between this vector and the vector v + Vec2.prototype.dist = function(v){ + var dx = this.x - v.x; + var dy = this.y - v.y; + return Math.sqrt(dx*dx + dy*dy); + }; + // return the distance between this vector and the vector of coordinates (x,y) + Vec2.prototype.dist_xy = function(x,y){ + var dx = this.x - x; + var dy = this.y - y; + return Math.sqrt(dx*dx + dy*dy); + }; + // return the squared distance between this vector and the vector and the vector v + Vec2.prototype.dist_sq = function(v){ + var dx = this.x - v.x; + var dy = this.y - v.y; + return dx*dx + dy*dy; + }; + // return the squared distance between this vector and the vector of coordinates (x,y) + Vec2.prototype.dist_sq_xy = function(x,y){ + var dx = this.x - x; + var dy = this.y - y; + return dx*dx + dy*dy; + }; + // return the dot product between this vector and the vector v + Vec2.prototype.dot = function(v){ + return this.x*v.x + this.y*v.y; + }; + // return the dot product between this vector and the vector of coordinate (x,y) + Vec2.prototype.dot_xy = function(x,y){ + return this.x*x + this.y*y; + }; + // return a new vector with the same coordinates as this + Vec2.prototype.clone = function(){ + return new Vec2(this.x,this.y); + }; + // return the sum of this and vector v as a new vector + Vec2.prototype.add = function(v){ + return new Vec2(this.x+v.x,this.y+v.y); + }; + // return the sum of this and vector (x,y) as a new vector + Vec2.prototype.add_xy = function(x,y){ + return new Vec2(this.x+x,this.y+y); + }; + // returns (this - v) as a new vector where v is a vector and - is the vector subtraction + Vec2.prototype.sub = function(v){ + return new Vec2(this.x-v.x,this.y-v.y); + }; + // returns (this - (x,y)) as a new vector where - is vector subtraction + Vec2.prototype.sub_xy = function(x,y){ + return new Vec2(this.x-x,this.y-y); + }; + // return (this * v) as a new vector where v is a vector and * is the by component product + Vec2.prototype.mult = function(v){ + return new Vec2(this.x*v.x,this.y*v.y); + }; + // return (this * (x,y)) as a new vector where * is the by component product + Vec2.prototype.mult_xy = function(x,y){ + return new Vec2(this.x*x,this.y*y); + }; + // return this scaled by float f as a new fector + Vec2.prototype.scale = function(f){ + return new Vec2(this.x*f, this.y*f); + }; + // return the negation of this vector + Vec2.prototype.neg = function(f){ + return new Vec2(-this.x,-this.y); + }; + // return this vector normalized as a new vector + Vec2.prototype.normalize = function(){ + var len = this.len(); + if(len == 0){ + return new Vec2(0,1); + }else if(len != 1){ + return this.scale(1.0/len); + } + return new Vec2(this.x,this.y); + }; + // return a new vector with the same direction as this vector of length float l. (negative values of l will invert direction) + Vec2.prototype.set_len = function(l){ + return this.normalize().scale(l); + }; + // return the projection of this onto the vector v as a new vector + Vec2.prototype.project = function(v){ + return v.set_len(this.dot(v)); + }; + // return a string representation of this vector + Vec2.prototype.toString = function(){ + var str = ""; + str += "["; + str += this.x; + str += ","; + str += this.y; + str += "]"; + return str; + }; + //return this vector counterclockwise rotated by rad radians as a new vector + Vec2.prototype.rotate = function(rad){ + var c = Math.cos(rad); + var s = Math.sin(rad); + var px = this.x * c - this.y *s; + var py = this.x * s + this.y *c; + return new Vec2(px,py); + }; + //return this vector counterclockwise rotated by deg degrees as a new vector + Vec2.prototype.rotate_deg = function(deg){ + return this.rotate(deg * deg2rad); + }; + //linearly interpolate this vector towards the vector v by float factor alpha. + // alpha == 0 : does nothing + // alpha == 1 : sets this to v + Vec2.prototype.lerp = function(v,alpha){ + var inv_alpha = 1 - alpha; + return new Vec2( this.x * inv_alpha + v.x * alpha, + this.y * inv_alpha + v.y * alpha ); + }; + // returns the angle between this vector and the vector (1,0) in radians + Vec2.prototype.angle = function(){ + return Math.atan2(this.y,this.x); + }; + // returns the angle between this vector and the vector (1,0) in degrees + Vec2.prototype.angle_deg = function(){ + return Math.atan2(this.y,this.x) * rad2deg; + }; + // returns true if this vector is equal to the vector v, with a tolerance defined by the epsilon module constant + Vec2.prototype.equals = function(v){ + if(Math.abs(this.x-v.x) > epsilon){ + return false; + }else if(Math.abs(this.y-v.y) > epsilon){ + return false; + } + return true; + }; + // returns true if this vector is equal to the vector (x,y) with a tolerance defined by the epsilon module constant + Vec2.prototype.equals_xy = function(x,y){ + if(Math.abs(this.x-x) > epsilon){ + return false; + }else if(Math.abs(this.y-y) > epsilon){ + return false; + } + return true; + }; +})(window); + +(function(window){ + // A Bounding Shapes Library + + + // A Bounding Ellipse + // cx,cy : center of the ellipse + // rx,ry : radius of the ellipse + function BEllipse(cx,cy,rx,ry){ + this.type = 'ellipse'; + this.x = cx-rx; // minimum x coordinate contained in the ellipse + this.y = cy-ry; // minimum y coordinate contained in the ellipse + this.sx = 2*rx; // width of the ellipse on the x axis + this.sy = 2*ry; // width of the ellipse on the y axis + this.hx = rx; // half of the ellipse width on the x axis + this.hy = ry; // half of the ellipse width on the y axis + this.cx = cx; // x coordinate of the ellipse center + this.cy = cy; // y coordinate of the ellipse center + this.mx = cx + rx; // maximum x coordinate contained in the ellipse + this.my = cy + ry; // maximum x coordinate contained in the ellipse + } + window.BEllipse = BEllipse; + + // returns an unordered list of vector defining the positions of the intersections between the ellipse's + // boundary and a line segment defined by the start and end vectors a,b + BEllipse.prototype.collide_segment = function(a,b){ + // http://paulbourke.net/geometry/sphereline/ + var collisions = []; + + if(a.equals(b)){ //we do not compute the intersection in this case. TODO ? + return collisions; + } + + // make all computations in a space where the ellipse is a circle + // centered on zero + var c = new Vec2(this.cx,this.cy); + a = a.sub(c).mult_xy(1/this.hx,1/this.hy); + b = b.sub(c).mult_xy(1/this.hx,1/this.hy); + + + if(a.len_sq() < 1 && b.len_sq() < 1){ //both points inside the ellipse + return collisions; + } + + // compute the roots of the intersection + var ab = b.sub(a); + var A = (ab.x*ab.x + ab.y*ab.y); + var B = 2*( ab.x*a.x + ab.y*a.y); + var C = a.x*a.x + a.y*a.y - 1; + var u = B * B - 4*A*C; + + if(u < 0){ + return collisions; + } + + u = Math.sqrt(u); + var u1 = (-B + u) / (2*A); + var u2 = (-B - u) / (2*A); + + if(u1 >= 0 && u1 <= 1){ + var pos = a.add(ab.scale(u1)); + collisions.push(pos); + } + if(u1 != u2 && u2 >= 0 && u2 <= 1){ + var pos = a.add(ab.scale(u2)); + collisions.push(pos); + } + for(var i = 0; i < collisions.length; i++){ + collisions[i] = collisions[i].mult_xy(this.hx,this.hy); + collisions[i] = collisions[i].add_xy(this.cx,this.cy); + } + return collisions; + }; + + // A bounding rectangle + // x,y the minimum coordinate contained in the rectangle + // sx,sy the size of the rectangle along the x,y axis + function BRect(x,y,sx,sy){ + this.type = 'rect'; + this.x = x; // minimum x coordinate contained in the rectangle + this.y = y; // minimum y coordinate contained in the rectangle + this.sx = sx; // width of the rectangle on the x axis + this.sy = sy; // width of the rectangle on the y axis + this.hx = sx/2; // half of the rectangle width on the x axis + this.hy = sy/2; // half of the rectangle width on the y axis + this.cx = x + this.hx; // x coordinate of the rectangle center + this.cy = y + this.hy; // y coordinate of the rectangle center + this.mx = x + sx; // maximum x coordinate contained in the rectangle + this.my = y + sy; // maximum x coordinate contained in the rectangle + } + + window.BRect = BRect; + // Static method creating a new bounding rectangle of size (sx,sy) centered on (cx,cy) + BRect.new_centered = function(cx,cy,sx,sy){ + return new BRect(cx-sx/2,cy-sy/2,sx,sy); + }; + //intersect line a,b with line c,d, returns null if no intersection + function line_intersect(a,b,c,d){ + // http://paulbourke.net/geometry/lineline2d/ + var f = ((d.y - c.y)*(b.x - a.x) - (d.x - c.x)*(b.y - a.y)); + if(f == 0){ + return null; + } + f = 1 / f; + var fab = ((d.x - c.x)*(a.y - c.y) - (d.y - c.y)*(a.x - c.x)) * f ; + if(fab < 0 || fab > 1){ + return null; + } + var fcd = ((b.x - a.x)*(a.y - c.y) - (b.y - a.y)*(a.x - c.x)) * f ; + if(fcd < 0 || fcd > 1){ + return null; + } + return new Vec2(a.x + fab * (b.x-a.x), a.y + fab * (b.y - a.y) ); + } + + // returns an unordered list of vector defining the positions of the intersections between the ellipse's + // boundary and a line segment defined by the start and end vectors a,b + + BRect.prototype.collide_segment = function(a,b){ + var collisions = []; + var corners = [ new Vec2(this.x,this.y), new Vec2(this.x,this.my), + new Vec2(this.mx,this.my), new Vec2(this.mx,this.y) ]; + var pos = line_intersect(a,b,corners[0],corners[1]); + if(pos) collisions.push(pos); + pos = line_intersect(a,b,corners[1],corners[2]); + if(pos) collisions.push(pos); + pos = line_intersect(a,b,corners[2],corners[3]); + if(pos) collisions.push(pos); + pos = line_intersect(a,b,corners[3],corners[0]); + if(pos) collisions.push(pos); + return collisions; + }; + + // returns true if the rectangle contains the position defined by the vector 'vec' + BRect.prototype.contains_vec = function(vec){ + return ( vec.x >= this.x && vec.x <= this.mx && + vec.y >= this.y && vec.y <= this.my ); + }; + // returns true if the rectangle contains the position (x,y) + BRect.prototype.contains_xy = function(x,y){ + return ( x >= this.x && x <= this.mx && + y >= this.y && y <= this.my ); + }; + // returns true if the ellipse contains the position defined by the vector 'vec' + BEllipse.prototype.contains_vec = function(v){ + v = v.mult_xy(this.hx,this.hy); + return v.len_sq() <= 1; + }; + // returns true if the ellipse contains the position (x,y) + BEllipse.prototype.contains_xy = function(x,y){ + return this.contains(new Vec2(x,y)); + }; + + +})(window); + + diff --git a/addons/web_diagram/static/src/xml/base_diagram.xml b/addons/web_diagram/static/src/xml/base_diagram.xml index 6d0e36e2f17..96e32f59648 100644 --- a/addons/web_diagram/static/src/xml/base_diagram.xml +++ b/addons/web_diagram/static/src/xml/base_diagram.xml @@ -1,6 +1,7 @@ <template> <t t-name="DiagramView"> <div class="oe_diagram_header" t-att-id="element_id + '_header'"> + <h3 class="oe_diagram_title"/> <div class="oe_diagram_buttons"> <button type="button" id="new_node" class="oe_button oe_diagram_button_new">New Node</button> </div> @@ -9,7 +10,11 @@ <span class="oe_pager_index">0</span> / <span class="oe_pager_count">0</span> </t> </div> + <div class="clear"></div> + </div> + + <div class="diagram-container"> + <div class="oe_diagram_diagram"/> </div> - <div id="dia-canvas" class="diagram" style="overflow: auto;"></div> </t> </template> |
