上节讲到如何创建组件,清除设计器视图,以及设计视图的持久化和恢复,本节将重点讲如何实现组件间的连线,前面章节有提到为了方便从持久化文件中恢复,组件和连线是分别存放的:nodes和lines对象,两个组件实现连线主要也还是通过鼠标拖动事件实现,但前提是有一个连接点的概念,即我们要从组件上、下、左、右四个锚点中开始拖动,在拖动过程中绘制跟随线,拖到目标组件上时出现锚点,在锚点上释放鼠标,在两个锚点间绘制连线,并将连线加到lines数组中。
下图是要实现的锚点图样例:
锚点为红色边框,整个组件可以作为一个锚点,同时四个x也可作为特定方位的锚点,锚点出现时鼠标为十字形状,代表允许按下鼠标拖动连线了。大家注意,我在打开按钮的边上增加了一个checkbox连线,用来指示是在连线状态与否,取消这个勾选,是不会出现连线锚点的,选择、拖动组件只有在非连线状态下进行,两者互斥。
function Connector(node) { this.node = node; this.group = null; } Connector.prototype = { destroy: function () { this.group.remove(); }, hiTest: function (event) { var bounds = this.node.getBound(); if (event.point.x >= bounds.x - 5 && event.point.x <= bounds.x + 5 && event.point.y >= bounds.y + bounds.height / 2 - 5 && event.point.y <= bounds.y + bounds.height / 2 + 5) { //在左连线指示器框中 this.group.children[0].bounds.x = bounds.x - 5; this.group.children[0].bounds.y = bounds.y + bounds.height / 2 - 5; this.group.children[0].bounds.width = 10; this.group.children[0].bounds.height = 10; } else if (event.point.x >= bounds.x + bounds.width - 5 && event.point.x <= bounds.x + bounds.width + 5 && event.point.y >= bounds.y + bounds.height / 2 - 5 && event.point.y <= bounds.y + bounds.height / 2 + 5) { //在右连线指示器框中 this.group.children[0].bounds.x = bounds.x + bounds.width - 5; this.group.children[0].bounds.y = bounds.y + bounds.height / 2 - 5; this.group.children[0].bounds.width = 10; this.group.children[0].bounds.height = 10; } else if (event.point.x >= bounds.x + bounds.width / 2 - 5 && event.point.x <= bounds.x + bounds.width / 2 + 5 && event.point.y >= bounds.y - 5 && event.point.y <= bounds.y + 5) { //在上连线指示器框中 this.group.children[0].bounds.x = bounds.x + bounds.width / 2 - 5; this.group.children[0].bounds.y = bounds.y - 5; this.group.children[0].bounds.width = 10; this.group.children[0].bounds.height = 10; } else if (event.point.x >= bounds.x + bounds.width / 2 - 5 && event.point.x <= bounds.x + bounds.width / 2 + 5 && event.point.y >= bounds.y + bounds.height - 5 && event.point.y <= bounds.y + bounds.height+ 5) { //在下连线指示器框中 this.group.children[0].bounds.x = bounds.x + bounds.width / 2 - 5; this.group.children[0].bounds.y = bounds.y + bounds.height - 5; this.group.children[0].bounds.width = 10; this.group.children[0].bounds.height = 10; } else { this.group.children[0].bounds.x = bounds.x this.group.children[0].bounds.y = bounds.y; this.group.children[0].bounds.width = bounds.width; this.group.children[0].bounds.height = bounds.height; } }, render: function () { var me = this; var color = 'white'; this.group = new paper.Group(); var rect = new paper.Path.Rectangle({ point: [this.node.getBound().x, this.node.getBound().y], size: [this.node.getBound().width, this.node.getBound().height], strokeColor: 'red', strokeWidth: 2 }) rect.onMouseDown = function (event) { debugger; }; this.group.addChild(rect); var bounds = this.node.getBound(); var topCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y - 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + 2.5], strokeColor: 'blue' }); this.group.addChild(topCross1); var topCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + 2.5],to: [bounds.x + bounds.width / 2 + 2.5, bounds.y - 2.5], strokeColor: 'blue' }); this.group.addChild(topCross2); var rightCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width - 2.5, bounds.y + bounds.height / 2 - 2.5], to: [bounds.x + bounds.width + 2.5, bounds.y + bounds.height / 2 + 2.5], strokeColor: 'blue' }); this.group.addChild(rightCross1); var rightCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width - 2.5, bounds.y + bounds.height / 2 + 2.5], to: [bounds.x + bounds.width + 2.5, bounds.y + bounds.height / 2 - 2.5], strokeColor: 'blue' }); this.group.addChild(rightCross2); var leftCross1 = new paper.Path.Line({ from: [bounds.x - 2.5, bounds.y + bounds.height / 2 - 2.5], to: [bounds.x + 2.5, bounds.y + bounds.height / 2 + 2.5], strokeColor: 'blue' }); this.group.addChild(leftCross1); var leftCross2 = new paper.Path.Line({ from: [bounds.x - 2.5, bounds.y + bounds.height / 2 + 2.5], to: [bounds.x + 2.5, bounds.y + bounds.height / 2 - 2.5], strokeColor: 'blue' }); this.group.addChild(leftCross2); var bottomCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + bounds.height - 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + bounds.height + 2.5], strokeColor: 'blue' }); this.group.addChild(bottomCross1); var bottomCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + bounds.height + 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + bounds.height - 2.5], strokeColor: 'blue' }); this.group.addChild(bottomCross2); this.group.bringToFront(); var drag = false; return this; } };
上面代码hiTest方法用于测式当前鼠标位置是显示哪个锚点:整个组件/上/下/左/右,并移动对应的锚点红色矩形。render画了一个红色矩形和四个连接点x。
在VisualDesigner中增加lining属性指示是否画线状态,
在Component中增加activeConnector指示当前活动的连接器锚点
在Component.init方法中增加了鼠标进入,退出后的连接点创建和删除,如下代码片断:
Component.prototype.init = function (options) { if (options == undefined) options = {}; this.properties = $.extend(options, Component.DEFAULTS); this.group = new paper.Group(); this.designer = undefined; //当前设计器,createElement时赋值 var me = this; var drag = false; this.activateConnector = null; //活动的连线指示符 this.group.onClick = function (event) { if (!me.designer.lining) //非画线状态才允许选中 me.group.children[0].selected = !me.group.children[0].selected; } this.group.onMouseDown = function (event) { if (!me.designer.lining) //非画线状态才允许拖动 drag = (event.event.button == 0); else { drawing = true; } } this.group.onMouseUp = function () { drag = false; document.body.style.cursor = 'default'; } this.group.onMouseDrag = function (event) { if (drag && !me.designer.lining) //非画线状态才允许拖动 { if (me.activateConnector) //在拖动元素时如果有连线指示器则清除。 { me.activateConnector.destroy(); me.activateConnector = null; } me.properties.x += event.delta.x; me.properties.y += event.delta.y; this.translate(event.delta.x, event.delta.y); document.body.style.cursor = 'move'; } } this.group.onMouseEnter = function (event) { if (!me.activateConnector && me.designer.lining) //还没有创建连接指示框,且当前为连线状态 { me.designer.selectAll(false);//取消选中所有元素,if any me.activateConnector = new Connector(me).render(); document.body.style.cursor = 'crosshair'; } } this.group.onMouseLeave = function (event) { if (me.designer.lining && me.activateConnector) { //当前为连线状态,且移出了组件范围 ,擦除连线指示框 me.activateConnector.destroy(); me.activateConnector = null; console.log("delete in group") document.body.style.cursor = 'default'; } } this.group.onMouseMove = function (event) { if (me.designer.lining && me.activateConnector) { //当前为连线状态,且在组件范围 ,检测四个边线连线指示框 me.activateConnector.hiTest(event) } } return this; }
这里说一个小插曲,因为要在组件的代码里访问设计器的成员(如是否画线状态visualDesigner.lining),我在Component里增加了一个designer对象来保存当前设计器,并在代码中访问,可保存设计视图时出现JSON对象序例化时出现递归的异常 ,因为序列化nodes组件对象数组时,每一个组件里有VisualDesigner对象而VisualDesigner对象里又有nodes对象的数组,首先想到的是特定的属性不要序列化,查资料后发现JSON.stringify里有第二个参数,可以为可序列化属性名称的白名单数组,也可以为函数,此外因为属性名称并不完全确定,所以用函数:
VisualDesigner.prototype.getContent = function () { debugger; return JSON.stringify({ "nodes": this.nodes, "lines": this.lines }, function (k, v) { if (k == "designer") { return undefined; } return v; }); }
依据面向对象的编程方法单一职责原则,增加了一个类(lineManager),专门用来管理连线的过程管理,在连线时要保持住前一个结点,在拖动结束时画出线,代码如下:
function LineManager(designer) { this.designer = designer; this.line = null;//当前跟随线 this.start = null;//当前正在画线的起点元素 this.startPos=null; var tool=new paper.Tool(); //设计器元素之外的移动也要显示跟随线, var me=this; tool.onMouseMove=function(event){ me.draging(event.point); } tool.onMouseUp=function(event) { //设计器元素之外的释放不生成连线,清除已有开始结点等信息, if (me.line) me.line.remove(); me.start=null; me.startPos=null; me.line=null; } } LineManager.prototype = { dragStart: function (co,pos) { this.start = co; var xy = co.node.getConnectorCenter(pos); //获取当前鼠标位置处连接点的中央坐标 this.startPos=xy; this.line = new paper.Path.Line({ from: [xy.x, xy.y], to: [xy.x, xy.y], strokeWidth: 2, strokeColor: 'red' }); }, draging: function (pos) { if (this.line !== null ) { var txy = this.calcLine(this.startPos.x, this.startPos.y, pos.x, pos.y); this.line.set({ pathData: 'M' + this.startPos.x + ',' + this.startPos.y + ' L' + txy.x + ',' + txy.y }); } }, dragEnd:function(co,pos) { var xy = co.node.getConnectorCenter(pos); //获取当前鼠标位置处连接点的中央坐标 if (this.line !== null ) { if (this.start.node.properties.id!=co.node.properties.id){ this.designer.createLine("曲线",{targetType:co.node.getConnectorDirection(this.startPos,pos),source:this.start.node.properties.id,target:co.node.properties.id,sxy:this.startPos,txy:xy}); } this.line.remove(); } this.start=null; //清除画线状态,等待重新画线 this.startPos=null; }, calcLine: function (x1, y1, x2, y2) { var vx = x2 - x1; var vy = y2 - y1; var d = Math.sqrt(vx * vx + vy * vy); vx /= d; vy /= d; d = Math.max(0, d - 5); return { 'x': Math.round(x1 + vx * d), 'y': Math.round(y1 + vy * d) } } }
同时增加了曲线的类(贝塞尔曲线),
function BezierLine() { } BezierLine.prototype = $.extend({}, Component.prototype); BezierLine.prototype = $.extend(BezierLine.prototype, { render: function (options) { this.properties.typeName = "曲线"; this.properties.strokeWidth = 2; this.properties.strokeColor = 'red'; this.properties=$.extend(this.properties,options) this.properties.x = Math.min(this.properties.sxy.x, this.properties.txy.x); this.properties.y = Math.min(this.properties.sxy.y, this.properties.txy.y); this.properties.width = Math.abs(this.properties.txy.x - this.properties.sxy.x); this.properties.height = Math.abs(this.properties.txy.y - this.properties.sxy.y); var wire = new paper.Path(this.calcPath(this.properties.targetType, this.properties.sxy.x, this.properties.sxy.y, this.properties.txy.x, this.properties.txy.y)); wire.strokeWidth = this.properties.strokeWidth; wire.strokeColor=this.properties.strokeColor; wire.sendToBack(); this.group=new paper.Group(); this.group.addChild(wire); //this.group.translate(this.properties.x, this.properties.y); return this; }, calcPath:function(type, x1, y1, x2, y2) { var path= ""; if(type =="left" || type == "right") path= 'M ' + x1 + ', ' + y1 + 'C ' + (x1 + (x2 - x1) / 2) + ', ' + y1 + ' ' + (x2 - (x2 - x1) / 2) + ', ' + y2 + ' ' + x2 + ', ' + y2; else if (type=="up" || type == "down") path='M' + x1 + ', ' + y1 + 'C ' + x1 + ', ' + (y1 + (y2 - y1) / 2) + ' ' + x2 + ', ' + (y2 - (y2 - y1) / 2) + ' ' + x2 + ', ' + y2; return path; } });
最后效果图如下:
源代码:sample.1.5.rar
(本文为原创,在引用代码和文字时请注明出处)