summaryrefslogtreecommitdiff
path: root/addons/web_diagram/static/src/js
diff options
context:
space:
mode:
authorXavier Morel <xmo@openerp.com>2012-03-22 15:33:31 +0100
committerXavier Morel <xmo@openerp.com>2012-03-22 15:33:31 +0100
commit7353e02e2ef4a403d4b79c1b157f2ca00b77d2fc (patch)
treea319552e3664ea272fdfb8abf31c961bcd5e1e6e /addons/web_diagram/static/src/js
parent514c63288c7d5d88d764b44696c5e3a5fc148dfb (diff)
parent1da49a44a57aa413e5eb96afe9c6d294e09848fd (diff)
[MERGE] from trunk
bzr revid: xmo@openerp.com-20120322143331-iajlifomw1hohbht
Diffstat (limited to 'addons/web_diagram/static/src/js')
-rw-r--r--addons/web_diagram/static/src/js/diagram.js352
-rw-r--r--addons/web_diagram/static/src/js/graph.js996
-rw-r--r--addons/web_diagram/static/src/js/vec2.js358
3 files changed, 1560 insertions, 146 deletions
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);
+
+