<@doc hierarchy="GMLDOM"> Aspect for drawing lines in a zoomable drawing. A line is a graphic used for drawing connections between shapes. (c) SAP AG 2003-2006. All rights reserved. /////////////////////////////////////////////////////////////////////// // ASPECT HEADER Aspect ZLine for ZDrawing; inherit ZGraphic; constructor(diagram, parent) this.supercall(); var src=base[base.@srcKey]; var srcpin = diagram.getPin(src.id); if (!srcpin) throw new Error('Source pin '+src.id+' not found'); this.srcpinID = srcpin.id; var trg=base[base.@trgKey]; var trgpin = diagram.getPin(trg.id); if (!trgpin) throw new Error('Target pin '+trg.id+' not found'); this.trgpinID = trgpin.id; srcpin.lines[this.id] = true; trgpin.lines[this.id] = true; this.lineRouting = GETVAR('SVG_ROUTING_MODE') || base.@routingMode || diagram.defaultRouting; this.lineColor = base.@strokeColor; this.hilightColor = diagram.base.@hilightColor; this.endMarker = (base.@endArrow ? 'endArrow'+base.@endArrow : ''); this.startMarker = (base.@startArrow ? 'startArrow'+base.@startArrow : ''); this.paint(); this.repaint(); end destructor this.hideSelectionEffect(); var canvas = this.canvas; var srcpin = this.getSrcpin(); var trgpin = this.getTrgpin(); if (srcpin) delete srcpin.lines[this.id]; if (trgpin) delete trgpin.lines[this.id]; SVG.removeElement(this.graphicNode); this.graphicNode = null; this.lineNode = null; this.labelNode = null; this.labelNode2 = null; this.isSelected = false; this.canvas.removeWidget(this.id); //TODO: delete this['base']; end virtual method getSrcpin() return this.canvas.getWidget(this.srcpinID); end virtual method getTrgpin() return this.canvas.getWidget(this.trgpinID); end ////////////////////////////////////////////////////////////////////////////////////// // BASE PROPERTIES // Line style <@doc group="Instance Properties"> Defines the line corners' radius This property controls the rounding of line corners for orthogonal line routing mode. By default, this property is set to 0 which results in square corners. property cornerRadius = 0; <@doc type="@LINE_ARROW" default="SVG_BLOCK_ARROW|SVG_FILLED"> Defines the line's end arrow property endArrow = 0; <@doc> Defines the line's dash pattern The ~dash property controls the pattern of dashes and gaps used to stroke the line. The ~dash property value can be either 'none' (indicating that the line is to be drawn solid), or a list of whitespace-separated numbers that specify the lengths of alternating dashes and gaps. property lineDash = 'none'; <@doc type="@LINE_ROUTING" default="SVG_INHERIT_LINE">Defines the line's routing constraints property routingMode = #[SVG_INHERIT_LINE]; <@doc> Defines line connection mode The connectionMode can be one of the following values: Value | Description SVG_CONNECT_PINS | The line will be drawn from source to target pins (default) SVG_CONNECT_SHAPES | The line will be drawn from source to target shapes property connectionMode = #[SVG_CONNECT_PINS]; <@doc type="@LINE_ARROW" default="SVG_NONE"> Defines the line's start arrow property startArrow = 0; // Line path <@doc scope="private"> Gets or sets the line's control points The value of this property depends on the specific type of line that uses it. property controls = ''; <@doc scope="private"> Gets or sets the line's label position and angle The value of this property depends on the specific type of line that uses it. property lpos = ''; <@doc scope="private"> Gets or sets the line's path The ~path property contains a whitespace-separated of path drawing commands relative to the source pin. property path = ''; <@struct name="LINE_ARROW" group="Structures"> A bitwise structure for defining line arrows A line arrow can be any bitwise combination of the following groups of flags: Name | Description SVG_NONE | No arrow | SVG_BLOCK_ARROW | Block arrow SVG_CLASSIC_ARROW | Classic arrow SVG_DIAMOND_ARROW | Diamond arrow SVG_CIRCLE_ARROW | Circle arrow SVG_DOUBLE_ARROW | Double arrow SVG_NOTCH_ARROW | Block arrow with a notch SVG_SINGLE_NOTCH | Single notch with no arrow SVG_DOUBLE_NOTCH | Double notch with no arrow | SVG_HOLLOW | The arrow head is hollow, i.e. filled with white color. SVG_FILLED | The arrow head is filled with the primary color. <@struct name="LINE_ROUTING"> A bitwise structure for defining line routing constraints A line routing can be any one of the following bitwise flags: Name | Description SVG_INHERIT_LINE | Inherit line routing mode from containing diagram | SVG_STRAIGHT_LINE | Straight line routing SVG_ANGULAR_LINE | Angular line routing SVG_CURVED_LINE | Curved line routing using quadratic bezier function SVG_ORTHOGONAL_LINE | Orthogonal line routing using only horizontal and vertical line segments ////////////////////////////////////////////////////////////////////////////////////// // STATIC PROPERTIES <@doc group="Aspect Properties"> Defines the line's source key The ~srcKey is the name of the property on the base object which points to the line's source element static readonly property srcKey = null; <@doc> Defines the line's target key The ~srcKey is the name of the property on the base object which points to the line's target element static readonly property trgKey = null; ////////////////////////////////////////////////////////////////////////////////////// // ASPECT PROPERTIES <@doc type="RGB" scope="private">Gets the line's color (inerited from the stroke color) virtual property lineColor = ''; <@doc type="RGB" scope="private">Gets the line's routing mode (inerited from the corresponding base property) virtual property lineRouting = #[SVG_STRAIGHT_LINE]; <@doc type="RGB" scope="private">Gets the end arrow marker (uncolored) virtual property endMarker = ''; <@doc type="RGB" scope="private">Gets the start arrow marker (uncolored) virtual property startMarker = ''; // Associated objects <@doc scope="private">Gets the source pin virtual property srcpinID = ''; <@doc scope="private">Gets the target pin virtual property trgpinID = ''; // Status flags <@doc type="b" scope="private">Indicates that this graphic is a line (for quick tests) virtual property isLine = true; // SVG painting objects <@doc type="SVGNode" scope="private">Gets the line node virtual property lineNode = null; <@doc type="SVGNode" scope="private">Gets the label node virtual property labelNode = null; <@doc type="SVGNode" scope="private">Gets the aditional label node virtual property labelNode2 = null; <@doc type="SVGNode []" scope="private">Gets the aditional body part nodes readonly property bodyParts = null; ////////////////////////////////////////////////////////////////////////////////////// // MODEL EVENTS override virtual method onModelUpdate(evt) this.repaint(evt.name); //TODO: repaint also: strokeColor, endArrow, startArrow, routingMode, cornerRadius, lineDash end override virtual method onModelInsert(evt) // nothing to do (line has no child graphics) end override virtual method onModelRemove(evt) // nothing to do (line has no child graphics) end ////////////////////////////////////////////////////////////////////////////////////// // PAINTING METHODS <@doc scope="private"> Paints the shape virtual method paint() var lineAddParts = this.parseParts(this.Class, base.Class); this.formulas = lineAddParts.formulas; this.graphicNode = SVG.createElement(this.getParent().linesLayer, 'a', { stroke: base.@strokeColor, fill: base.@strokeColor }); this.graphicNode.peerID = this.id; // create line node var diag = this.getDiagram(), cp = this.getConnectionPoints(); var fragment = ''); var sParts = lineAddParts.shapeParts; for (var m in sParts) { var sPart = sParts[m]; var svgElem = SVG.createElement(this.graphicNode, sPart.type, sPart.props); if (this.bodyParts == null) this.bodyParts = []; this.bodyParts.push(svgElem); } this.rerouteBodyParts(); end <@doc scope="private"> Repaints the line after a property has changed virtual method repaint(prop) if (!prop) { this.reroute(); } if (!prop || prop == 'name') { var name=base.name; if (ISNULL(name)) { if (this.labelNode) { SVG.removeElement(this.labelNode); this.labelNode = null; } } else { if (!this.labelNode) { var lpos=SPLITN(base.@lpos,3); this.labelNode = SVG.createElement(this.graphicNode, 'text', { 'class':'lineText', x:lpos[0], y:lpos[1]-3, transform:'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'}); } SVG.setText(this.labelNode, name); } } if (!prop || prop == 'guard') { var guard=base.guard || ''; if (guard == 'true') { if (this.labelNode2) { SVG.removeElement(this.labelNode2); this.labelNode2 = null; } return; } // String.fromCharCode(60) == '<' String.fromCharCode(62) == '>' if (guard != 'false' && guard != '') guard = String.fromCharCode(60) + String.fromCharCode(60) + 'cond' + String.fromCharCode(62) + String.fromCharCode(62); if (ISNULL(guard)) { if (this.labelNode2) { SVG.removeElement(this.labelNode2); this.labelNode2 = null; } } else { if (!this.labelNode2) { var lpos=SPLITN(base.@lpos,3); this.labelNode2 = SVG.createElement(this.graphicNode, 'text', { 'class':'lineText', x:lpos[0], y:lpos[1]+9, transform:'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'}); } SVG.setText(this.labelNode2, guard); } } end <@doc scope="private"> Reroutes the line after either of its endpoints was repositioned virtual method reroute() // get connection points var cp = this.getConnectionPoints(); // apply routing method this['reroute'+this.lineRouting](cp.p1,cp.p2); // repaint line and label this.lineNode.setAttribute ('d', 'M'+cp.p1.x+','+cp.p1.y+base.@path); if (this.labelNode) { var lpos=SPLITN(base.@lpos,3); this.labelNode.setAttribute('x', lpos[0]); this.labelNode.setAttribute('y', lpos[1]-3); this.labelNode.setAttribute('transform', 'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'); } if (this.labelNode2) { var lpos=SPLITN(base.@lpos,3); this.labelNode2.setAttribute('x', lpos[0]); this.labelNode2.setAttribute('y', lpos[1] + 9); this.labelNode2.setAttribute('transform', 'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'); } this.rerouteBodyParts(); end <@doc scope="private"> Reroutes the body parts after either of its endpoints was repositioned virtual method rerouteBodyParts() var lpos=SPLITN(base.@lpos,3); var aspectData = {w: lpos[0] * -2, h: lpos[1] * -2}; // the (*-2) is due to passing defineShape x1/y1 agrs for (var k in this.formulas) { var formula = this.formulas[k]; if (formula) formula(this.bodyParts, aspectData, base); } end ////////////////////////////////////////////////////////////////////////////////////// // INTERACTIVE EFFECTS override virtual method showHoverEffect(); if (this.isSelected || this.marked) return; SVG.setProperty(this.graphicNode, 'stroke', this.hilightColor); SVG.setProperty(this.graphicNode, 'fill', this.hilightColor); end override virtual method hideHoverEffect(); if (this.isSelected || this.marked) return; SVG.setProperty(this.graphicNode, 'stroke', base.@strokeColor); SVG.setProperty(this.graphicNode, 'fill', base.@strokeColor); end override virtual method showSelectionEffect() if (this.isSelected) return; SVG.raiseElement(this.graphicNode); this.showHoverEffect(); var diag = this.getDiagram(); if (this.endMarker) { var marker = diag.getColoredMarker(this.endMarker, this.hilightColor); this.lineNode.setAttribute('marker-end', marker); } if (this.startMarker) { var marker = diag.getColoredMarker(this.startMarker, this.hilightColor); this.lineNode.setAttribute('marker-start', marker); } if (base.@connectionMode == #[SVG_CONNECT_PINS]) { this.getSrcpin().showSelectionEffect(true); this.getTrgpin().showSelectionEffect(true); } this.isSelected = true; end override virtual method hideSelectionEffect() if (!this.isSelected) return; this.isSelected = false; this.hideHoverEffect(); var diag = this.getDiagram(); if (this.endMarker) { var marker = diag.getColoredMarker(this.endMarker, this.lineColor); this.lineNode.setAttribute('marker-end', marker); } if (this.startMarker) { var marker = diag.getColoredMarker(this.startMarker, this.lineColor); this.lineNode.setAttribute('marker-start', marker); } if (base.@connectionMode == #[SVG_CONNECT_PINS]) { this.getSrcpin().hideSelectionEffect(true); this.getTrgpin().hideSelectionEffect(true); } end override virtual method showFocusEffect() this.inFocus = true; end override virtual method hideFocusEffect() this.inFocus = false; end override virtual method flyIntoView() var b = this.getBBox(), p = this.getParent(); return this.canvas.flyToArea({x:p.qx+b.x, y:p.qy+b.y, w:b.width, h:b.height}); end ////////////////////////////////////////////////////////////////////////////////////// // EDITING METHODS <@doc scope="private"> Starts a line rename operation virtual method startRename() var diag = this.getDiagram(); var lbox = this.getLabelPos(); var cbox = this.canvas.canvasToClient(lbox.x, lbox.y, lbox.width, lbox.height); var cpos = this.board.clientToBrowser({x:cbox.x, y:cbox.y}); var data = { object: base, graphic: this, attr: 'name', callback: diag.endRename, delegate: diag, select: true, x: cpos.x+cbox.w/2, centerX:true, y: cpos.y+cbox.h/2, centerY:true, w: cbox.w, h: cbox.h }; this.board.textedit.show(data); end ////////////////////////////////////////////////////////////////////////////////////// // MOUSE EVENTS virtual method setupPointer(evt) var diag=this.getDiagram(); diag.selectById(this.id, evt.ctrlKey); if (evt.detail>1) { RULE('drillIntoShape', diag.base, base); } else if (evt.button == 2) { var pos = this.canvas.clientToCanvas(evt.clientX, evt.clientY); var menu = RULE('defineContextMenu', diag.base, diag.boardAspect, this.base, pos); if (menu) diag.openMenu(menu, pos, menu.callback); } return null; end virtual method mouseover(evt, ptr) if (ptr) return; var diag=this.getDiagram(); diag.showTooltip(this); if (diag.hilightMode) this.showHoverEffect(); end virtual method mouseout(evt, ptr) if (ptr) return; this.getDiagram().hideTooltip(); this.hideHoverEffect(); end virtual method onHandleMove(ptr) var func='onHandleMove'+this.lineRouting; switch (ptr.phase) { case #[PTR_START]: ptr.scrollMode = #[PTR_AUTOSCROLL]; ptr.handle = ptr.source.getAttribute('handle'); var parent=this.getParent(), qx=parent.qx, qy=parent.qy, s=this.canvas.scale; ptr.wire = SVG.createElement(this.getDiagram().wiresLayer, 'path', { fill:'none', 'stroke':this.hilightColor, 'stroke-width':2/s, 'stroke-dasharray':2/s, transform:'translate('+qx+' '+qy+')', 'pointer-events':'none' }); ptr.hx = FLOAT(ptr.source.getAttribute('cx')) - ptr.pos.x; ptr.hy = FLOAT(ptr.source.getAttribute('cy')) - ptr.pos.y; this[func](ptr); break; case #[PTR_FIRSTMOVE]: SVG.setProperty(this.lineNode, 'stroke-opacity', 0.4); break; case #[PTR_MOVE]: this[func](ptr); break; case #[PTR_LASTMOVE]: SVG.setProperty(this.lineNode, 'stroke-opacity', 1); break; case #[PTR_FINISH]: BEGIN('#TEXT[XMIT_TRANS_MOVE_LINK]'.replace('{0}', base.Class.metadata.title)); this[func](ptr); this.updateLineData(ptr.path, ptr.controls); this.reroute(); COMMIT(); // fall-through case #[PTR_CANCEL]: SVG.removeElement(ptr.wire); if (!ptr.isOverSource) SVG.setProperty(ptr.source, 'class', 'handleNormal'); this.getDiagram().adjustHandles(); break; } end ////////////////////////////////////////////////////////////////////////////////////// // GEOMETRIC CALCULATIONS <@doc scope="private"> Gets the control handles data virtual method getHandlesData() var data=this['getHandlesData'+this.lineRouting](), p=this.getParent(); if (!data) return null; for (var k in data) { var d=data[k]; d.cx = p.qx + d.cx*p.qs; d.cy = p.qy + d.cy*p.qs; } return data; end <@doc scope="private"> Gets the label positioning data virtual method getLabelPos() var diag = this.getDiagram(); var label = this.labelNode; var bbox = label && label.getBBox() || null; if (bbox && bbox.width>0) { var x=bbox.x, y=bbox.y, w=bbox.width, h=bbox.height; } else { if (!base.@lpos) this.reroute(); var lpos=SPLITN(base.@lpos,3); var w=75, h=16, x=lpos[0]-w/2, y=lpos[1]-h/2; } var p=this.getParent(); x = p.qx + x*p.qs; y = p.qy + y*p.qs; return {x:x, y:y, width:w, height:h}; end <@doc scope="private"> Gets the tooltip positioning data virtual method getTooltipPos() var b=this.getLabelPos(); return {x:b.x+b.width/2, y:b.y+b.height, align:'center below', offset:4}; end <@doc scope="private"> Gets the line's bounding box virtual method getBBox() var b=this.lineNode.getBBox(), p=this.getParent(); return {x:b.x*p.qs, y:b.y*p.qs, width:b.width*p.qs, height:b.height*p.qs}; end <@doc scope="private"> Tests whether the line is enclosed by a given rectangle virtual method enclosedIn(rect) var b = this.getBBox(), p = this.getParent(); var x1=p.qx+b.x, x2=x1+b.width, y1=p.qy+b.y, y2=y1+b.height; return (rect.x <= x1 && rect.x+rect.w >= x2 && rect.y <= y1 && rect.y+rect.h >= y2); end <@doc scope="private"> Tests whether the line intersects a given rectangle virtual method intersects(rect) if (this.enclosedIn(rect)) return true; return false; end <@doc scope="private"> Converts an array of path drawing commands to string virtual method path2str() var A=arguments, len=A.length; var B=new Array(len); for (var i=0; i Silently updates the line path data virtual method updateLineData(path, controls, lpos) BEGIN('#TEXT[XMIT_TRANS_MOVE_LINK]'.replace('{0}', "'" + base.Class.metadata.title + "'")); // As an optimization, the rerouting methods call this method to update the line path silently. // This is allowed since line routing is always invoked inside the current transaction scope. base.setProperty('@path', path||'', true); base.setProperty('@controls', controls||'', true); if (lpos != undefined) base.setProperty('@lpos', lpos, true); COMMIT(); end <@doc scope="private"> Gets calculated line connection points, taking connection mode into account virtual method getConnectionPoints() var diag = this.getDiagram(); var srcpin = this.getSrcpin(); var trgpin = this.getTrgpin(); var srcshape = srcpin.getShape(); var trgshape = trgpin.getShape(); var useSrcShape = false, useTrgShape = false; if (base.@connectionMode == #[SVG_CONNECT_SHAPES]) { useSrcShape = true; useTrgShape = true; } if (diag.isZoomable) { // TODO: BlockZoom work for (var b = srcshape.getParent(); b && b.isBlock && !b.isExpanded; b = b.getParent()) { srcshape = b; useSrcShape = true; } for (var b = trgshape.getParent(); b && b.isBlock && !b.isExpanded; b = b.getParent()) { trgshape = b; useTrgShape = true; } } var p1 = (useSrcShape) ? srcshape.getLineCP(this, trgshape) : srcpin.getLineCP(this, trgpin); var p2 = (useTrgShape) ? trgshape.getLineCP(this, srcshape) : trgpin.getLineCP(this, srcpin); var count = 1; if (useSrcShape || useTrgShape) { // prevent line overlapping - count all lines that have same srcshape and trgshape as this line. // the offset is calculated basing on the order of this line among all similar other lines. var srcbase = srcshape.base, trgbase = trgshape.base; var parent = diag.base.getCommonParent(srcbase, trgbase); var block = diag.getGraphic(parent.id); // diag.getGraphic returns null before diag.paint is finished. In such case - recursively find the block among parents if (!block) for (block = srcshape; block && (block.id != parent.id); block = block.getParent()); var lines = block && block.base[block.base.@linesKey]; var canvas = this.canvas; for (var k in lines) { var ln = canvas.getWidget(k); if (!ln) continue; if (ln.id == this.id) break; var src = ln.getSrcpin().getShape(), trg = ln.getTrgpin().getShape(); var srcsrc = (srcbase.id == src.base.id), trgtrg = (trgbase.id == trg.base.id); var srctrg = (srcbase.id == trg.base.id), trgsrc = (trgbase.id == src.base.id); if (diag.isZoomable) { // TODO: BlockZoom - for collapsed block - count inner lines as well if (srcshape.isBlock && !srcshape.isExpanded) { srcsrc = srcbase.contains(src.base); srctrg = srcbase.contains(trg.base); } if (trgshape.isBlock && !trgshape.isExpanded) { trgsrc = trgbase.contains(src.base); trgtrg = trgbase.contains(trg.base); } } if (srcsrc && trgtrg || srctrg && trgsrc) count++; } var dy = count * 12; var p1Shape = canvas.getWidget(p1.shapeID), p2Shape = canvas.getWidget(p2.shapeID); p1.y = (dy < p1Shape.h) ? p1.y + dy : p1.y + p1Shape.h; p2.y = (dy < p2Shape.h) ? p2.y + dy : p2.y + p2Shape.h; } var pts = (useSrcShape) ? perimeterCP(srcshape, this.getParent(), p1, p2) : null; var ptt = (useTrgShape) ? perimeterCP(trgshape, this.getParent(), p2, p1) : null; // TODO: the following attempts to implement heuristics for too-close srcshape/trgshape. Finish it. /* if (pts && ptt && base.name && (SVG.getDistance(pts, ptt) < 60)) { var pts = topCP(srcshape, this.getParent()), ptt = topCP(trgshape, this.getParent()); var sign = SIGN(ptt.x - pts.x), dxs = MIN(count * 12, srcshape.w/2), dxt = MIN(count * 12, trgshape.w/2); pts.x += dxs * sign; ptt.x -= dxt * sign; } */ if (pts) { p1.x = pts.x; p1.y = pts.y; p1.rot = pts.rot; } if (ptt) { p2.x = ptt.x; p2.y = ptt.y; p2.rot = ptt.rot; } return {p1: p1, p2: p2}; function perimeterCP(shape, block, p1, p2) { // get shape's rect in block's coordinate system var rect = {x: shape.ox + shape.x1*shape.os - block.qx, y: shape.oy + shape.y1*shape.os - block.qy, w: shape.w, h: shape.h}; var pt = SVG.intersection(rect, p1, p2); // convert direction character to rotation property value // shape.rot is ignored because the line is between given points, not from a pin on shape's specific side var dir2rot = {T: 1, R: 0, B: 3, L: 2}; var rot = Math.round(dir2rot[pt.dir]); return {x: pt.x, y: pt.y, rot: rot}; } function topCP(shape, block) { // get shape's rect in block's coordinate system var rect = {x: shape.ox + shape.x1*shape.os - block.qx, y: shape.oy + shape.y1*shape.os - block.qy, w: shape.w, h: shape.h}; pt = {x: rect.x + rect.w/2, y: rect.y, dir: 'T'}; // convert direction character to rotation property value var dir2rot = {T: 1, R: 0, B: 3, L: 2}; var rot = Math.round(dir2rot[pt.dir]); return {x: pt.x, y: pt.y, rot: rot}; } end ////////////////////////////////////////////////////////////////////////////////////// // STRAIGHT LINE ROUTING virtual method reroute#[SVG_STRAIGHT_LINE](p1,p2) // calculate line path var path = 'l '+(p2.x-p1.x)+' '+(p2.y-p1.y); // calculate label position var lx = (p1.x+p2.x)/2; var ly = (p1.y+p2.y)/2; var la = Math.atan2(p2.y-p1.y, p2.x-p1.x)*SVG.R2D; if (Math.abs(la) > 90) la += 180; // update line data this.updateLineData(path, '', this.path2str(lx,ly,la)); return path; end <@doc scope="private"> Gets the control handles data virtual method getHandlesData#[SVG_STRAIGHT_LINE]() return null; end virtual method onHandleMove#[SVG_STRAIGHT_LINE](ptr) end ////////////////////////////////////////////////////////////////////////////////////// // ANGULAR LINE ROUTING virtual method reroute#[SVG_ANGULAR_LINE](p1,p2) // calculate line path var controls=base.@controls, C=SPLITN(controls); var dx=p2.x-p1.x, dy=p2.y-p1.y; if (C.length==5 && C[0]==(p2.rot*4+p1.rot)) { var cx1=C[1], cy1=C[2], cx2=C[3], cy2=C[4]; } else { var leg1=20*p1.scale, leg2=20*p2.scale; var cx1=(p1.rot&1 ? 0 : leg1*(1-p1.rot)), cy1=(p1.rot&1 ? leg1*(p1.rot-2) : 0); var cx2=(p2.rot&1 ? 0 : leg2*(1-p2.rot)), cy2=(p2.rot&1 ? leg2*(p2.rot-2) : 0); controls = ''; } var path = this.path2str('l', cx1, cy1, cx2-cx1+dx, cy2-cy1+dy, -cx2, -cy2); // calculate label position var x1 = p1.x+cx1; var x2 = p2.x+cx2; var y1 = p1.y+cy1 var y2 = p2.y+cy2; var lx = (x1+x2)/2; var ly = (y1+y2)/2; var la = Math.atan2(y2-y1, x2-x1)*SVG.R2D; if (Math.abs(la) > 90) la += 180; // update line data this.updateLineData(path, controls, this.path2str(lx,ly,la)); return path; end virtual method getHandlesData#[SVG_ANGULAR_LINE]() var path=SPLITN(base.@path); if (path.length != 7) return null; var cp = this.getConnectionPoints(); return { C1: {cx:path[1]+cp.p1.x,cy:path[2]+cp.p1.y}, C2: {cx:cp.p2.x-path[5],cy:cp.p2.y-path[6]} } end virtual method onHandleMove#[SVG_ANGULAR_LINE](ptr) switch (ptr.phase) { case #[PTR_START]: var path=SPLITN(base.@path); if (path.length != 7) { ptr.cancel=true; break; } var cp = this.getConnectionPoints(); ptr.p1 = cp.p1; ptr.p2 = cp.p2; ptr.cx1 = path[1]; ptr.cy1 = path[2]; ptr.cx2 = -path[5]; ptr.cy2 = -path[6]; break; case #[PTR_MOVE]: var p1=ptr.p1, p2=ptr.p2, dx=p2.x-p1.x, dy=p2.y-p1.y, x=ptr.pos.x, y=ptr.pos.y; var cx1=ptr.cx1, cy1=ptr.cy1, cx2=ptr.cx2, cy2=ptr.cy2, hx=0, hy=0; if (ptr.handle == 'C1') { if (p1.rot & 1) { cy1+=ptr.offset.y; x=p1.x+cx1; hy=1; } else { cx1+=ptr.offset.x; y=p1.y+cy1; hx=1; } } else if (ptr.handle == 'C2') { if (p2.rot & 1) { cy2+=ptr.offset.y; x=p2.x+cx2; hy=1; } else { cx2+=ptr.offset.x; y=p2.y+cy2; hx=1; } } ptr.path = this.path2str('l', cx1, cy1, cx2-cx1+dx, cy2-cy1+dy, -cx2, -cy2); ptr.controls = this.path2str(p2.rot*4+p1.rot, cx1, cy1, cx2, cy2); ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+ptr.path); hx = (hx ? ptr.pos.x : ptr.origin.x); hy = (hy ? ptr.pos.y : ptr.origin.y); this.getDiagram().moveHandle(ptr.handle, hx+ptr.hx, hy+ptr.hy); break; } end ////////////////////////////////////////////////////////////////////////////////////// // CURVED LINE ROUTING virtual method reroute#[SVG_CURVED_LINE](p1,p2) // calculate line path var controls=base.@controls, C=SPLITN(controls); if (C.length==5 && C[0]==(p2.rot*4+p1.rot)) { var cx1=C[1], cy1=C[2], cx2=C[3], cy2=C[4]; } else { var leg1=20*p1.scale, leg2=20*p2.scale; var cx1=(p1.rot==0 ? leg1 : p1.rot==2 ? -leg1 : 0), cy1=(p1.rot==3 ? leg1 : p1.rot==1 ? -leg1 : 0); var cx2=(p2.rot==0 ? leg2 : p2.rot==2 ? -leg2 : 0), cy2=(p2.rot==3 ? leg2 : p2.rot==1 ? -leg2 : 0); controls = ''; } // calculate radius around p1 and p2 so that the curve starts after the marker // TODO: same radius calculation is better be applied to other line types as well var rad1=10*p1.scale, rad2=10*p2.scale; var rw1=(p1.rot==0 ? rad1 : p1.rot==2 ? -rad1 : 0), rh1=(p1.rot==3 ? rad1 : p1.rot==1 ? -rad1 : 0); var rw2=(p2.rot==0 ? -rad2 : p2.rot==2 ? rad2 : 0), rh2=(p2.rot==3 ? -rad2 : p2.rot==1 ? rad2 : 0); var dx=p2.x-p1.x-rw1-rw2, dy=p2.y-p1.y-rh1-rh2; var mx=(cx1+cx2+dx)/2, my=(cy1+cy2+dy)/2; var path = this.path2str('l', rw1, rh1, 'q', cx1, cy1, mx, my, 'q', cx2+dx-mx, cy2+dy-my, dx-mx, dy-my, 'l', rw2, rh2); // calculate label position var x1 = p1.x+rw1+cx1; var x2 = p2.x-rw2+cx2; var y1 = p1.y+rh1+cy1 var y2 = p2.y-rh2+cy2; var lx = (x1+x2)/2; var ly = (y1+y2)/2; var la = Math.atan2(y2-y1, x2-x1)*SVG.R2D; if (Math.abs(la) > 90) la += 180; // update line data this.updateLineData(path, controls, this.path2str(lx,ly,la)); return path; end virtual method getHandlesData#[SVG_CURVED_LINE]() var path=SPLITN(base.@path); if (path.length != 16) return null; var cp = this.getConnectionPoints(), p1 = cp.p1; return { C1: {cx:path[1]+path[4]+p1.x,cy:path[2]+path[5]+p1.y}, C2: {cx:path[1]+path[6]+path[9]+p1.x,cy:path[2]+path[7]+path[10]+p1.y} } end virtual method onHandleMove#[SVG_CURVED_LINE](ptr) switch (ptr.phase) { case #[PTR_START]: var path=SPLITN(base.@path); if (path.length != 16) { ptr.cancel=true; break; } var cp = this.getConnectionPoints(); ptr.p1 = cp.p1; ptr.p2 = cp.p2; ptr.rw1 = path[1]; ptr.rh1 = path[2]; ptr.rw2 = path[14]; ptr.rh2=path[15]; ptr.dx = ptr.p2.x-ptr.p1.x-ptr.rw1-ptr.rw2; ptr.dy = ptr.p2.y-ptr.p1.y-ptr.rh1-ptr.rh2; ptr.cx1 = path[4]; ptr.cy1 = path[5]; ptr.cx2 = path[6]+path[9]-ptr.dx; ptr.cy2 = path[7]+path[10]-ptr.dy; break; case #[PTR_MOVE]: var p1=ptr.p1, p2=ptr.p2, dx=ptr.dx, dy=ptr.dy; var cx1=ptr.cx1, cy1=ptr.cy1, cx2=ptr.cx2, cy2=ptr.cy2; switch (ptr.handle) { case 'C1': cx1 += ptr.offset.x; cy1 += ptr.offset.y; break; case 'C2': cx2 += ptr.offset.x; cy2 += ptr.offset.y; break; } var mx=(cx1+cx2+dx)/2, my=(cy1+cy2+dy)/2; ptr.path = this.path2str('l', ptr.rw1, ptr.rh1, 'q', cx1, cy1, mx, my, 'q', cx2+dx-mx, cy2+dy-my, dx-mx, dy-my, 'l', ptr.rw2, ptr.rh2); ptr.controls = this.path2str(p2.rot*4+p1.rot, cx1, cy1, cx2, cy2); ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+ptr.path); this.getDiagram().moveHandle(ptr.handle, ptr.pos.x+ptr.hx, ptr.pos.y+ptr.hy); break; } end ////////////////////////////////////////////////////////////////////////////////////// // ORTHOGONAL LINE ROUTING virtual method reroute#[SVG_ORTHOGONAL_LINE](p1,p2) // get line's coordinate system (p1, p2 are relative to it) var parent = this.getParent(); var pqx = parent.qx, pqy = parent.qy; // normalize points (so that source point is at origin and oriented at 0 degrees) var S1 = this.canvas.getWidget(p1.shapeID); var ox1 = S1.ox, oy1=S1.oy, os1=S1.os; var spx = p1.x - (ox1+S1.cx*os1 - pqx); // source point, relative to source shape center var spy = p1.y - (oy1+S1.cy*os1 - pqy); var Sx = ox1+(S1.cx+spx)*os1; var Sy = oy1+(S1.cy+spy)*os1; var Sz = p1.rot; var Sx1 = ox1+S1.x1*os1-Sx; // source shape bounds var Sy1 = oy1+S1.y1*os1-Sy; var Sx2 = ox1+S1.x2*os1-Sx; var Sy2 = oy1+S1.y2*os1-Sy; var S2 = this.canvas.getWidget(p2.shapeID); var ox2 = S2.ox, oy2=S2.oy, os2=S2.os; var tpx = p2.x - (ox2+S2.cx*os2 - pqx); // target point, relative to target shape center var tpy = p2.y - (oy2+S2.cy*os2 - pqy); var Tx = ox2+(S2.cx+tpx)*os2-Sx; var Ty = oy2+(S2.cy+tpy)*os2-Sy; var Tz = p2.rot; var Tx1 = ox2+S2.x1*os2-Sx; // target shape bounds var Ty1 = oy2+S2.y1*os2-Sy; var Tx2 = ox2+S2.x2*os2-Sx; var Ty2 = oy2+S2.y2*os2-Sy; var f1=((Sz%2) == 1), f2=(Sz==1 || Sz==2); var t, mx, my; if (f1) { t=Tx; Tx=Ty; Ty=t; t=Sx1; Sx1=Sy1; Sy1=t; t=Sx2; Sx2=Sy2; Sy2=t; t=Tx1; Tx1=Ty1; Ty1=t; t=Tx2; Tx2=Ty2; Ty2=t; } if (f2) { Tx=-Tx; Ty=-Ty; Sx1=-Sx1; Sy1=-Sy1; Sx2=-Sx2; Sy2=-Sy2; Tx1=-Tx1; Ty1=-Ty1; Tx2=-Tx2; Ty2=-Ty2; } t=Math.max(Sx1,Sx2); Sx1=Math.min(Sx1,Sx2); Sx2=t; t=Math.max(Sy1,Sy2); Sy1=Math.min(Sy1,Sy2); Sy2=t; t=Math.max(Tx1,Tx2); Tx1=Math.min(Tx1,Tx2); Tx2=t; t=Math.max(Ty1,Ty2); Ty1=Math.min(Ty1,Ty2); Ty2=t; var mr=10, MR=40; // minimum and maximum shape margins (absolute) var Sa=(-Sy1)/(Sy2-Sy1), Sm=Math.max(mr,MR*Sa)*os1; // source margins (relative and absolute) var Ta=(Ty-Ty1)/(Ty2-Ty1), Tm=Math.max(mr,MR*Ta)*os2; // target margins (relative and absolute) Tz = (Tz-Sz)%4; if (Tz<0) Tz+=4; // compute path layout var path=[], contour=0; // pins are in opposite directions if (Tz == 2) { var flag = (Ty>0); if (Tx > 0) { // pins are facing each other if (Ty==0) { // trivial path (1 segment) contour = 11; path.push('h',Tx); } else { // path without obstacles (3 segments) contour = (flag ? 12 : 13); mx = Tx*(flag ? 1-Sa : Sa); path.push('h',mx,'v',Ty,'h',Tx-mx); } } else { // pins are facing opposite directions var my1=(flag ? Ty1 : Sy1), my2=(flag ? Sy2 : Ty2), mx1, mx2; if (my1 > my2) { // path snakes through vertical space between shapes (5 segments) contour = (flag ? 14 : 15); mx1 = (flag ? MR-Sm : Sm)*1.5; mx2 = (MR*1.5-mx1); my = my1+(my2-my1)*Sa; } else { // path encircles shapes (5 segments) contour = 16; mx2 = (flag ? MR-Sm : Sm)*1.5; mx1 = Math.max(0,Tx2)+mx2; my = (flag ? Ty2+(MR-Sm)*1.5 : Ty1-Sm*1.5); } path.push('h',mx1,'v',my,'h',Tx-(mx1+mx2),'v',Ty-my,'h',mx2); } // pins are in same direction } else if (Tz == 0) { if (Tx>0 && Ty1*Ty2<=0) { // path with bypass for target shape (5 segments) var flag = (Math.abs(Ty1) < Math.abs(Ty2)), mx1, mx2; contour = (flag ? 21 : 22); mx1 = Tx1*(flag ? Sa : 1-Sa); mx2 = (flag ? MR-Sm : Sm)*1.5; my = (flag ? Ty1-(MR-Sm)*1.5 : Ty2+Sm*1.5); path.push('h',mx1,'v',my,'h',Tx-mx1+mx2,'v',Ty-my,'h',-mx2); } else if (Tx=Sy1 && Ty<=Sy2) { // path with bypass for source shape (5 segments) var flag = (Math.abs(Sy1) < Math.abs(Sy2)), mx1, mx2; contour = (flag ? 23 : 24); mx1 = Tx+(Sx1-Tx)*(flag ? 1-Sa : Sa); mx2 = Sm*1.5; my = (flag ? Sy1-mx2 : Sy2+mx2); path.push('h',mx2,'v',my,'h',mx1-mx2,'v',Ty-my,'h',Tx-mx1); } else { // path without obstacles (3 segments) var flag = (Ty>0); contour = (flag ? 25 : 26); mx = (flag ? (MR-Sm) : Sm)*1.5+Math.max(Tx,0); path.push('h',mx,'v',Ty,'h',Tx-mx); } // pins are perpendicular } else { var flag = ((Sz&1) ? (Tz==3) : (Tz==1)); var sign = (flag ? 1 : -1), my1=(flag ? Ty1 : Sy1), my2=(flag ? Sy2 : Ty2); if (Tx>0 && sign*Ty>0) { // path without obstacles (2 segments) contour = 31; path.push('h',Tx,'v',Ty); } else if (Tx<=0 && my1>my2) { // path snakes through vertical space between shapes (4 segments) contour = (flag ? 32 : 33); mx = (flag ? MR-Sm : Sm)*1.5; my = my1+(my2-my1)*Sa; path.push('h',mx,'v',my,'h',Tx-mx,'v',Ty-my); } else if (Tx1>0) { // path snakes through horizontal space between shapes (4 segments) contour = (flag ? 34 : 35); mx=Tx1*(flag ? Sa : 1-Sa); my=(flag ? MR-sign*Sm : sign*Sm)*1.5; path.push('h',mx,'v',Ty-my,'h',Tx-mx,'v',my); } else { // path encircles shapes (4 segments) contour = (flag ? 36 : 37); my=(flag ? Math.min(Sy1,Ty1)-Sm*1.5 : Math.max(Sy2,Ty2)+(MR-Sm)*1.5); mx=Math.max(0,Tx2)+(flag ? Sm : MR-Sm)*1.5; path.push('h',mx,'v',my,'h',Tx-mx,'v',Ty-my); } } // rotate path to original orientation for (var i=0, len=path.length, sign=(f2 ? -1 : 1); i= 3) { if (path[0]=='h') lx+=path[1]; else ly+=path[1]; } if (ln >= 5) { if (path[2]=='h') lx+=path[3]; else ly+=path[3]; } if (path[ln-1]=='h') { lx += path[ln]/2; } else { if (ABS(path[ln]) >= lmin) { ly += path[ln]/2; la = SIGN(path[ln])*90; } else if (ln >= 3 && ABS(path[ln-2]) >= lmin) { lx -= path[ln-2]/2; } else if (ln <= path.length-2 && ABS(path[ln+2]) >= lmin) { ly += path[ln]; lx += path[ln+2]/2; } else { ly += path[ln]/2; } } lx += p1.x; ly += p1.y; // apply round corners var r=base.@cornerRadius||0; if (r > 0) path = this.applyRoundCorners(path, r); // update line data path = path.join(' '); this.updateLineData(path, controls.join(' '), this.path2str(lx,ly,la)); return path; end virtual method getHandlesData#[SVG_ORTHOGONAL_LINE]() var path=SPLIT(base.@path), len=path.length; if (len <= 4) return null; var cp = this.getConnectionPoints(), p1 = cp.p1, cx=p1.x, cy=p1.y, data={}; for (var i=0; i0) data['C'+(i/2)] = {cx:cx+dx/2, cy:cy+dy/2} cx+=dx; cy+=dy; } return data; end virtual method onHandleMove#[SVG_ORTHOGONAL_LINE](ptr) switch (ptr.phase) { case #[PTR_START]: var path=SPLIT(base.@path), len=path.length; var n=POS((ptr.source.getAttribute('handle')||'').charAt(1)), axis=path[2*(n-1)]; if (axis != 'h' && axis != 'v') { ptr.cancel=true; break; } for (var i=0; i 0) path = this.applyRoundCorners(path, r); ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+path.join(' ')); this.getDiagram().moveHandle('C'+n, org.x+dx+ptr.hx, org.y+dy+ptr.hy); break; case #[PTR_FINISH]: var controls=SPLITN(base.@controls); controls[ptr.n] += ptr.d0; ptr.path = ptr.path.join(' '); ptr.controls = controls.join(' '); break; } end virtual method applyRoundCorners(path, radius) var R=[], p1=[], p2=[], len=path.length-2; for (var i=0; i0 ? 0 : 1)+' '+d2+' '+d1); } p1[i+3] -= d2; } for (var len=p1.length; i Shows the line's movable effect virtual method showMovableEffect() if (this.getDiagram().alphaMode) SVG.setProperty(this.graphicNode, 'opacity', 1); end <@doc scope="private"> Hides the line's movable effect virtual method hideMovableEffect() if (this.getDiagram().alphaMode) SVG.setProperty(this.graphicNode, 'opacity', 0.2); end