From 32a6aa7b656804c95b8a2a2df06900955c6df44b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Hannu=20Niemist=C3=B6?= Date: Tue, 2 May 2017 20:04:35 +0300 Subject: [PATCH] (refs #7177) Rounded connections Adds HasRounding property to G2D that rounds connections in the diagrams. Change-Id: I2f5429f90e926b9569633056d50a233cf9f4c395 --- .../rendering/BasicConnectionStyle.java | 96 +++++++++++++++++- .../diagram/adapter/RouteGraphUtils.java | 6 +- .../diagram/connection/ConnectionVisuals.java | 17 ++-- .../query/ConnectionVisualsRequest.java | 5 +- bundles/org.simantics.g2d.ontology/graph.tg | Bin 18788 -> 18938 bytes .../graph/G2D.pgraph | 2 + .../simantics/diagram/stubs/G2DResource.java | 6 ++ .../g2d/nodes/connection/RouteGraphNode.java | 4 +- .../tests/TestRouteGraphNodeApplet.java | 2 +- 9 files changed, 122 insertions(+), 16 deletions(-) diff --git a/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/BasicConnectionStyle.java b/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/BasicConnectionStyle.java index a8661d324..b2895fb34 100644 --- a/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/BasicConnectionStyle.java +++ b/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/BasicConnectionStyle.java @@ -13,10 +13,13 @@ package org.simantics.diagram.connection.rendering; import java.awt.Color; import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.Stroke; +import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; import java.io.Serializable; /** @@ -32,18 +35,25 @@ public class BasicConnectionStyle implements ConnectionStyle, Serializable { final double branchPointRadius; final Stroke lineStroke; final Stroke routeLineStroke; - final double degenerateLineLength; + final double degenerateLineLength; + final double rounding; transient Line2D line = new Line2D.Double(); transient Ellipse2D ellipse = new Ellipse2D.Double(); - public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength) { + public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength, + double rounding) { this.lineColor = lineColor; this.branchPointColor = branchPointColor; this.branchPointRadius = branchPointRadius; this.lineStroke = lineStroke; this.routeLineStroke = routeLineStroke; this.degenerateLineLength = degenerateLineLength; + this.rounding = rounding; + } + + public BasicConnectionStyle(Color lineColor, Color branchPointColor, double branchPointRadius, Stroke lineStroke, Stroke routeLineStroke, double degenerateLineLength) { + this(lineColor, branchPointColor, branchPointRadius, lineStroke, routeLineStroke, degenerateLineLength, 0.0); } public Color getLineColor() { @@ -88,7 +98,83 @@ public class BasicConnectionStyle implements ConnectionStyle, Serializable { g.setColor(lineColor); if (lineStroke != null) g.setStroke(lineStroke); - g.draw(path); + if(rounding > 0.0) { + Object oldRenderingHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.draw(round(path)); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldRenderingHint); + } + else + g.draw(path); + } + + private Path2D round(Path2D path) { + Path2D newPath = new Path2D.Double(); + PathIterator it = path.getPathIterator(new AffineTransform()); + double[] coords = new double[6]; + double newX=0.0, newY=0.0; + double curX=0.0, curY=0.0; + double oldX=0.0, oldY=0.0; + int state = 0; + while(!it.isDone()) { + int type = it.currentSegment(coords); + if(type == PathIterator.SEG_LINETO) { + newX = coords[0]; + newY = coords[1]; + if(state == 1) { + double dx1 = curX-oldX; + double dy1 = curY-oldY; + double dx2 = curX-newX; + double dy2 = curY-newY; + double maxRadius = 0.5 * Math.min(Math.sqrt(dx1*dx1 + dy1*dy1), Math.sqrt(dx2*dx2 + dy2*dy2)); + double radius = Math.min(rounding, maxRadius); + newPath.lineTo(curX + radius*Math.signum(oldX-curX), curY + radius*Math.signum(oldY-curY)); + newPath.curveTo(curX, curY, + curX, curY, + curX + radius*Math.signum(newX-curX), curY + radius*Math.signum(newY-curY)); + + //newPath.lineTo(curX + round*Math.signum(oldX-curX), curY + round*Math.signum(oldY-curY)); + //newPath.lineTo(curX + round*Math.signum(newX-curX), curY + round*Math.signum(newY-curY)); + //newPath.lineTo(curX, curY); + } + else + ++state; + oldX = curX; + oldY = curY; + curX = newX; + curY = newY; + } + else { + if(state > 0) { + newPath.lineTo(curX, curY); + state = 0; + } + switch(type) { + case PathIterator.SEG_MOVETO: + curX = coords[0]; + curY = coords[1]; + newPath.moveTo(curX, curY); + break; + case PathIterator.SEG_QUADTO: + curX = coords[2]; + curY = coords[3]; + newPath.quadTo(coords[0], coords[1], coords[2], coords[3]); + break; + case PathIterator.SEG_CUBICTO: + curX = coords[4]; + curY = coords[5]; + newPath.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); + break; + case PathIterator.SEG_CLOSE: + newPath.closePath(); + break; + } + } + it.next(); + } + if(state > 0) + newPath.lineTo(curX, curY); + return newPath; } @Override @@ -170,4 +256,8 @@ public class BasicConnectionStyle implements ConnectionStyle, Serializable { return true; } + public double getRounding() { + return rounding; + } + } diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/RouteGraphUtils.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/RouteGraphUtils.java index 99d9acb70..60f7a09db 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/RouteGraphUtils.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/RouteGraphUtils.java @@ -564,7 +564,7 @@ public class RouteGraphUtils { Color branchPointColor = Color.BLACK; double branchPointRadius = 0.5; double degenerateLineLength = 0.8; - + Color lineColor = cv != null ? cv.toColor() : null; if (lineColor == null) lineColor = Color.DARK_GRAY; @@ -572,6 +572,7 @@ public class RouteGraphUtils { if (lineStroke == null) lineStroke = new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10, null, 0); Stroke routeLineStroke = GeometryUtils.scaleStrokeWidth(lineStroke, 2); + double rounding = cv.rounding == null ? 0.0 : cv.rounding; return new BasicConnectionStyle( lineColor, @@ -579,7 +580,8 @@ public class RouteGraphUtils { branchPointRadius, lineStroke, routeLineStroke, - degenerateLineLength); + degenerateLineLength, + rounding); } public static void scheduleSynchronize(Session session, Resource connection, RouteGraphChangeEvent event) { diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/connection/ConnectionVisuals.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/connection/ConnectionVisuals.java index 5aa8048ac..45e407906 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/connection/ConnectionVisuals.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/connection/ConnectionVisuals.java @@ -25,13 +25,15 @@ public class ConnectionVisuals { public final float[] color; public final StrokeType strokeType; public final Stroke stroke; + public final Double rounding; - public ConnectionVisuals(float[] color, StrokeType strokeType, Stroke stroke) { + public ConnectionVisuals(float[] color, StrokeType strokeType, Stroke stroke, Double rounding) { if (color != null && color.length < 3) throw new IllegalArgumentException("colors must have at least 3 components (rgb), got " + color.length); this.color = color; this.strokeType = strokeType; this.stroke = stroke; + this.rounding = rounding; } public Color toColor() { @@ -47,6 +49,7 @@ public class ConnectionVisuals { final int prime = 31; int result = 1; result = prime * result + Arrays.hashCode(color); + result = prime * result + ((rounding == null) ? 0 : rounding.hashCode()); result = prime * result + ((stroke == null) ? 0 : stroke.hashCode()); result = prime * result + ((strokeType == null) ? 0 : strokeType.hashCode()); return result; @@ -58,20 +61,22 @@ public class ConnectionVisuals { return true; if (obj == null) return false; - if (!(obj instanceof ConnectionVisuals)) + if (getClass() != obj.getClass()) return false; ConnectionVisuals other = (ConnectionVisuals) obj; if (!Arrays.equals(color, other.color)) return false; + if (rounding == null) { + if (other.rounding != null) + return false; + } else if (!rounding.equals(other.rounding)) + return false; if (stroke == null) { if (other.stroke != null) return false; } else if (!stroke.equals(other.stroke)) return false; - if (strokeType == null) { - if (other.strokeType != null) - return false; - } else if (!strokeType.equals(other.strokeType)) + if (strokeType != other.strokeType) return false; return true; } diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/query/ConnectionVisualsRequest.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/query/ConnectionVisualsRequest.java index 3e70f1210..4af440a52 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/query/ConnectionVisualsRequest.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/query/ConnectionVisualsRequest.java @@ -52,8 +52,9 @@ public class ConnectionVisualsRequest extends ResourceRead { StrokeType strokeType = toStrokeType(g.getPossibleObject(structuralConnectionType, g2d.HasStrokeType)); Stroke stroke = G2DUtils.getStroke(g, g.getPossibleObject(structuralConnectionType, g2d.HasStroke)); - - return new ConnectionVisuals(color, strokeType, stroke); + Double rounding = g.getPossibleRelatedValue(structuralConnectionType, g2d.HasRounding, Bindings.DOUBLE); + + return new ConnectionVisuals(color, strokeType, stroke, rounding); } StrokeType toStrokeType(Resource strokeType) { diff --git a/bundles/org.simantics.g2d.ontology/graph.tg b/bundles/org.simantics.g2d.ontology/graph.tg index b75d2eb3ac6e587cf58d374f0a03f1a19a3f30a2..a9eea99f0e275eb450d7dddf58f7e392553cf9d3 100644 GIT binary patch literal 18938 zcmeI4d3>Bz)yHSj>`U8pV=aB4#ZqVsEvqQlq-je^+k~W4_VQ%rN%GLiOqgfVHlR`! zM0QXFTo6G76x>+#RRly)p^E#y@4m9{JN|y>xhFZfA9;EI=0iW9o4Ma}e)pWaJ$I(< z+|&$|!{Nb9CX+?U)bWS=Q`1t8lF~>y7cDQ16f0FUH%414Q89^2#iW5;%OB1B(HQ5W zVkNG`QBqH~k*ex7QW_*5^~Xg>OT{Ki-Jj~}o>HkYpOE3zJ;zyGgd{# zs2u0I!!kyX9#8Mk3NmQZG}2dy+REjSeO;d#JCmNMfVIf4FTuE7 zj5E=@?s91ugBa}?+b(dwP29CBxYrotTf)sEkMeO2abSIiSSKfOai9=s9N4Fdy}l!? zgl<;nrxvtd6ZS;~^nE8<=9|8+N+siAvgv51^p2o^W%eEGC=>A`kv5xoyn6m)#nS@1G`d;@KO-rmkQ-nH|K^(mT>;=PtXOn44r$`Ls)qCZ|S(Hf#0+F-|kH z+*VmzN-CV|v@cE4mvsBUxM$j69==4I8=krhkE1t?#GFb?ztZ<}Zk^4#Aio4IdQ4R6K1t9CGP{k@j;oJrA0= z@}yRA=WpPl<@R%{1egqKN0j8su^fOH)~>X-n&$sbg`LpxXo!lrQ5oMcVsBbe92vsU zxUYvs0L~X^Tv;fE6@9eN5nGMekbSNoH+NUxDLD7Wx5qovNR^P= zR35#mddT!R+L`X1Ot%k9BbD62uCof&x5d*imIJ2hN%)biTeY;qotLX=sBWZYILxId z$V;@XHYu(Y(JY9LD3#;WO2tZ8Xe-16MZQ@;dzSdw%kD6yCEG;HSti78B57D(4vR^D zsXPStJ~Zt%DPF`~`WRs!(_pT8_k-h6!d&CsU%dyy@f2Zy<6WZO1K@a)u*7)xRqy_A zJVDskco(Uc?>~xR z3$-#FiVHXiv-LC&8QmSJ4o^uqQmEhnD{bO)afz7OllAxrf`*f6xWqKA`a;@pd z%_3e@#tmDKQlF*Nj^I)V>Tsp)-Us_(%kzkF)R zNf;iiYvVk6Qo^I3t)($%lHDe%R-K^kLYR}!SkSi8b(_)NDq0Rh=c&7t$j3scr_y<= zQ7;qql!=4pfS;vfnNcnmCHG`|shp3>I+-ufzT61AMaVt5mgo4Icqr!9%Fx_xbf=51 zo-?=_D*^AjX*sJm9c|3aCCOeIu+f83C z5@~uw=HnRMZKmZSBk2-}Jd^Syyfe;M_zC}2+PjSKC=v2B9N!$%#n#k=LmHn53(?XJ zG`dlYwbD6G`G&ONga_?7+BP(7xetf4V9qq<5|GoadlPmlOSIN|Uxao~g!-0{lqTwW?=oyP+w#7;@*61m-PLTVnBTNRn`umdz`z!Mb&Ff68BSptg zYw2Nki}2)h^y5!Z=FZfPcs*f)4cj>s%4c?2vpY?|i$y3Og2KumKRICtEi_(iR40py zTd5ash8(55U!H6va^6?hx$_6@50y4Gy@R3rog@cLtQ8H% z%DsV!lfMzYOzT=BJ6vS4Ms7UM&~mtuoG21K7|okpQ5^D?!%27T?-6T5XR;wqV*F(c zGdzo?drU|%$j7Adm4Aq$Sw=Kl0x_@m_Vh3vvyF0-D66?(@N5*~o{@v8ho3RgiAgkX zG_9@>U4#AP^)Z^Rs9lb?JNdiF$A#;0l*aDF`9x}I50eNMzSVzJEOl!lycP3MYCM7? zhGPf3?t<4x@H*4u86Hpfc$&wp9#8dnipP^ZZt#`^hBFqZ2) z=D(>}e~ri09&-+@p8r^4nIF9^11)D>_4pN!U-tMVk6-lo1&^Qi_&JZC_4pZ&pZ54E zkDv7T36CH5_%V+k_4r}K|3>{GkN+3^0QO~o*B_yfYhJ}4ynf&DgYeHr{Z^EJp;$fH z>dC6V7WIFkd>ifO;`JKG-vZA=oqLmhZGSK7|3Fbak36#KOOCDn??#VL$r-Zh`T2=# z^?w6ve11eFtDb*(B3u1m!FIf4^>cp6I$rkgFDO<|wtBMa*}p$~da~7%RnPwY$s%(>dC6_LH)O$ zo^17G)gOiWZ#+HO>dC6_M*Y{Go^17G)pw!(D^E|hda~-*q5exxPquop>er(F3r|nB zda~+QqyBSGPquop>N`>YnWraPJz4e3QU9r@CtE#P_3fzt#OQgRkgcAq`Zm;mjAHj2 z+3LxvUxxY*QB=?UMz(sg>JLZ#eiVJ)cXzDMd*E2x-wXa8it6V$R{dnhR{ve2pMg5r z>d99B9iyLuI@#*UR(}t8C!Ci%;4f;B}s3)$i%}AiSc;XAkOcLVZWacY`(F z*{G9kJhF{P)_5~fCu=+E zI473B4Ay*Hzb~PD0ebW)LmyfFSEK$#!_#z7MGd5ZEH=zDG!;DWG z=1ukwePr$bXHmZ%McY$pFXUs~dBXbT zC^{aF_cD(^;_;;(f7s&>d3*_2=Z`+J?kBz{YuvLR@VM-zUkNX-sIS>cLJO0SD;=t%>J|Jc$Hh-`w#n0 zo1IUx&L?A8=GZNBZY*=|$l4$Ff7meRr-Y*Oak*oij|Rs&AIdf!+v|LAykt8cMX<)_ z+LCR1vbJYm$=ZHB>O&~n{xZkfzk0{oKV=)A?KM8fOV;@FP%ogUo_?n)o{q}Dj|J~z%IJWD33T(DNvEc#K2R)|EwkNy!5v^s$BJ2KRy&uKq zi@@{U^Fbfk#mm+?*7;V}_Iosa$CYkTIS-sU6Q ze0^ZmGhc|J`RF5Sz8c4xPg&bDAN4jL+2%VLtnvBYPuBR=jy1lrjZeMC?{In>eg=_9M3`<1NYV|%iWkMr4sqV3OcY}?bX?fL#p zw(XAsYx^@%C)@U9ZNJU=ZToK6wEcOglXX5(6+Q^BD4bql8=rcOe~$BOe6C*?ipD?J zvCj9aj$M5DHwEf#e6o#Cw(-}2_58mOb+X2P#j(a$w(+UA@yRy+TCm2y0Clp)f7!9d zSGMt~*ZAi&aJpQf6zjLhPV_#OI=z8(9 zJNYK$-x04bIo9ziYyY|5sn_vxKalNsJB^=XBl~_EkABVfqSI@C$VZ^4p7Xa7#m*mD z=j-Rrulx(gI)27lj-ung-m&I;!Ljy7S@U!JY_H?L+OZv9J8T;NI@HM;|9Qt6U)jc| z-o_`}_-)3&0(G+QxAExLe9t+(_J_O-Mf34|Cfj&q8;`8whSUuK(K{Z+Fkn4?X^|$3OA-ryl>zvF;b~UKB-q`ezMKK zpYb#Qz9=?7S@Tm%*8ER6*8IwvkNK%rW`45Gzu5She-Vn!PuBd@k~RP1jy1os=3{>9 zm6@Mx^Di`h=HCa!<|k`@YRQ`aF~^!;S@SVJ^~%gow)yume&(NxV)K(VKec4d|EOcl zudMl)pL%8HC)@mc7(esx?(rPMQ&HzL(S-Aaug#9{12fL|Jigzt_LsTYPMLFInQLR2 zd&M&Mu4O)3yxJo!{o037gF~!!YN2I*R6NcC7RLUB}u##+rs=<4rNlcK%RI|1AM7#TJFNduj}P~FnaBJr zVB5EQyxilJ9(Q`Y+T$ZVUh6SGd)xe79(Q|ul*c_D_jKFQ;a9-r)S z=yA^D$m0QzH+ekdana+F$ESK+@%VI)w|RV)#}|2gvB#Hqe1*qXdVH0~S9^S|$2WL< zqsKRUe2d4odVHJ5w|jht$9H;sm&bQ|e2-&2KWf2Q6g@Av*AGMaIxCrXY~#<&*94@B z|MA9AlRguC%{s|ft^O;FnwjmcaRll2V1q4jWe`8`b@bq{UmY&e_XFVCkuSQ~j&W6buMG_^`TEjYI=IABq(G&Ec);!CMP`>GzvR4NBy67=&%RKm|kpAzvj z{X%+Rzts9k@y!^&Yc#=oxp?Kv&O!RU>C9zfYU0;v1HDL-`G^>6+&8&1mkRDEU#{hN z(@C$Cf%!FoN5rm{VN0z;)6}lmwN*uo9G`VO;#hyr5wbz2NuXp++w%E;7E68w`gP ze4Dq(X}Uo2-jreDkY9Zw!R|0dkgc6PaD3&Wj~B>2ks^ zzWY&2!{MnTXjIGx6<**)m8aJNV@=Zdk}?R>q1@jRf8C^PVJluFG1b8r9I_zycK1f_oX=osNBX$)zNQR-ON;l3k)h(Eq3{$;w%jJ;uy&UuEgLYKFpu4e z-Brx1uvF=RgS_r)#Q4*a{(^yEudNG57w|m-4@nZ`Mk?`UoVC)yAeAuBObJ+Mo8ZZh z2b=KdRkoBCZ+Q$G(-j_P94Od3 z@faJd2>;9N!~b%I_J$red8_aHE%BJ8)s4FZ|eU6T+^1y literal 18788 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 L0.Double G2D.LineEnd