From 29a3aa63e021911c441b5258ea168d95e126449d Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 15 Dec 2023 10:12:54 +0000 Subject: [PATCH 01/21] :construction: WIP --- debug/init.js | 12 + .../renderer/base/coord-ele-math/coords.js | 3 +- .../base/coord-ele-math/edge-endpoints.js | 7 +- src/extensions/renderer/base/node-shapes.js | 62 +++--- .../renderer/canvas/drawing-images.js | 5 +- .../renderer/canvas/drawing-nodes.js | 10 +- .../renderer/canvas/drawing-shapes.js | 208 ++++++++++++++---- src/extensions/renderer/canvas/node-shapes.js | 8 +- src/math.js | 12 +- src/style/properties.js | 3 + 10 files changed, 241 insertions(+), 89 deletions(-) diff --git a/debug/init.js b/debug/init.js index 85b7479378..c9f8957459 100644 --- a/debug/init.js +++ b/debug/init.js @@ -10,6 +10,18 @@ var cy, defaultSty, options; .style({ 'label': 'data(id)' }) + .selector('node#a') + .style({ + 'shape': 'round-rectangle', + 'width': 35, + 'corner-radius': 200 + }) + .selector('node#b') + .style({ + 'shape': 'round-triangle', + 'width': 40, + 'corner-radius': 4 + }) .selector('edge') .style({ diff --git a/src/extensions/renderer/base/coord-ele-math/coords.js b/src/extensions/renderer/base/coord-ele-math/coords.js index 2c1830298c..b724e42511 100644 --- a/src/extensions/renderer/base/coord-ele-math/coords.js +++ b/src/extensions/renderer/base/coord-ele-math/coords.js @@ -134,6 +134,7 @@ BRp.findNearestElements = function( x, y, interactiveElementsOnly, isTouch ){ var hw = width / 2; var hh = height / 2; var pos = node.position(); + var cornerRadius = node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue; if( pos.x - hw <= x && x <= pos.x + hw // bb check x @@ -143,7 +144,7 @@ BRp.findNearestElements = function( x, y, interactiveElementsOnly, isTouch ){ var shape = r.nodeShapes[ self.getNodeShape( node ) ]; if( - shape.checkPoint( x, y, 0, width, height, pos.x, pos.y ) + shape.checkPoint( x, y, 0, width, height, pos.x, pos.y, cornerRadius ) ){ addEle( node, 0 ); return true; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js index cda4965213..560a5f7561 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js @@ -79,8 +79,11 @@ BRp.findEndpoints = function( edge ){ let overrideEndpts = self || taxi; let srcManEndpt = edge.pstyle('source-endpoint'); let srcManEndptVal = overrideEndpts ? 'outside-to-node' : srcManEndpt.value; + let srcCornerRadius = source.pstyle('corner-radius').value === 'auto' ? 'auto' : source.pstyle('corner-radius').pfValue; let tgtManEndpt = edge.pstyle('target-endpoint'); let tgtManEndptVal = overrideEndpts ? 'outside-to-node' : tgtManEndpt.value; + let tgtCornerRadius = target.pstyle('corner-radius').value === 'auto' ? 'auto' : target.pstyle('corner-radius').pfValue; + rs.srcManEndpt = srcManEndpt; rs.tgtManEndpt = tgtManEndpt; @@ -125,7 +128,7 @@ BRp.findEndpoints = function( edge ){ target.outerHeight(), p1_i[0], p1_i[1], - 0 + 0, tgtCornerRadius ); if( tgtManEndptVal === 'outside-to-node-or-label' || tgtManEndptVal === 'outside-to-line-or-label' ){ @@ -217,7 +220,7 @@ BRp.findEndpoints = function( edge ){ source.outerHeight(), p2_i[0], p2_i[1], - 0 + 0, srcCornerRadius ); if( srcManEndptVal === 'outside-to-node-or-label' || srcManEndptVal === 'outside-to-line-or-label' ){ diff --git a/src/extensions/renderer/base/node-shapes.js b/src/extensions/renderer/base/node-shapes.js index 0c0e5ccca8..007a976389 100644 --- a/src/extensions/renderer/base/node-shapes.js +++ b/src/extensions/renderer/base/node-shapes.js @@ -10,11 +10,11 @@ BRp.generatePolygon = function( name, points ){ points: points, - draw: function( context, centerX, centerY, width, height ){ + draw: function( context, centerX, centerY, width, height, cornerRadius ){ this.renderer.nodeShapeImpl( 'polygon', context, centerX, centerY, width, height, this.points ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ return math.polygonIntersectLine( x, y, this.points, @@ -25,7 +25,7 @@ BRp.generatePolygon = function( name, points ){ ; }, - checkPoint: function( x, y, padding, width, height, centerX, centerY ){ + checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ return math.pointInsidePolygon( x, y, this.points, centerX, centerY, width, height, [0, -1], padding ) ; @@ -39,11 +39,11 @@ BRp.generateEllipse = function(){ name: 'ellipse', - draw: function( context, centerX, centerY, width, height ){ + draw: function( context, centerX, centerY, width, height, cornerRadius ){ this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ return math.intersectLineEllipse( x, y, nodeX, @@ -53,7 +53,7 @@ BRp.generateEllipse = function(){ ; }, - checkPoint: function( x, y, padding, width, height, centerX, centerY ){ + checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ return math.checkInEllipse( x, y, width, height, centerX, centerY, padding ); } } ); @@ -95,24 +95,24 @@ BRp.generateRoundPolygon = function( name, points ){ points: allPoints, - draw: function( context, centerX, centerY, width, height ){ - this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points ); + draw: function( context, centerX, centerY, width, height, cornerRadius ){ + this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points, cornerRadius ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ return math.roundPolygonIntersectLine( x, y, this.points, nodeX, nodeY, width, height, - padding ) + padding, cornerRadius ) ; }, - checkPoint: function( x, y, padding, width, height, centerX, centerY ){ + checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ return math.pointInsideRoundPolygon( x, y, this.points, - centerX, centerY, width, height) + centerX, centerY, width, height, cornerRadius) ; } } ); @@ -126,24 +126,24 @@ BRp.generateRoundRectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), - draw: function( context, centerX, centerY, width, height ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height ); + draw: function( context, centerX, centerY, width, height, cornerRadius ){ + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, cornerRadius ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ return math.roundRectangleIntersectLine( x, y, nodeX, nodeY, width, height, - padding ) + padding, cornerRadius ) ; }, checkPoint: function( - x, y, padding, width, height, centerX, centerY ){ + x, y, padding, width, height, centerX, centerY, cornerRadius ){ - var cornerRadius = math.getRoundRectangleRadius( width, height ); + cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius; var diam = cornerRadius * 2; // Check hBox @@ -213,8 +213,8 @@ BRp.generateCutRectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), - draw: function( context, centerX, centerY, width, height ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height ); + draw: function( context, centerX, centerY, width, height, cornerRadius ){ + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height); }, generateCutTrianglePts: function( width, height, centerX, centerY ){ @@ -235,7 +235,7 @@ BRp.generateCutRectangle = function(){ }; }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ var cPts = this.generateCutTrianglePts( width + 2*padding, height+2*padding, nodeX, nodeY ); var pts = [].concat.apply([], [cPts.topLeft.splice(0, 4), cPts.topRight.splice(0, 4), @@ -245,7 +245,7 @@ BRp.generateCutRectangle = function(){ return math.polygonIntersectLine( x, y, pts, nodeX, nodeY ); }, - checkPoint: function( x, y, padding, width, height, centerX, centerY ){ + checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ // Check hBox if( math.pointInsidePolygon( x, y, this.points, centerX, centerY, width, height - 2 * this.cornerLength, [0, -1], padding ) ){ @@ -275,11 +275,11 @@ BRp.generateBarrel = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), - draw: function( context, centerX, centerY, width, height ){ + draw: function( context, centerX, centerY, width, height, cornerRadius ){ this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ // use two fixed t values for the bezier curve approximation var t0 = 0.15; @@ -343,7 +343,7 @@ BRp.generateBarrel = function(){ }, checkPoint: function( - x, y, padding, width, height, centerX, centerY ){ + x, y, padding, width, height, centerX, centerY, cornerRadius){ var curveConstants = math.getBarrelCurveConstants( width, height ); var hOffset = curveConstants.heightOffset; @@ -424,11 +424,11 @@ BRp.generateBottomRoundrectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), - draw: function( context, centerX, centerY, width, height ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height ); + draw: function( context, centerX, centerY, width, height, cornerRadius ){ + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, cornerRadius ); }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding ){ + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ var topStartX = nodeX - ( width / 2 + padding ); var topStartY = nodeY - ( height / 2 + padding ); var topEndY = topStartY; @@ -445,14 +445,14 @@ BRp.generateBottomRoundrectangle = function(){ nodeX, nodeY, width, height, - padding ) + padding, cornerRadius ) ; }, checkPoint: function( - x, y, padding, width, height, centerX, centerY ){ + x, y, padding, width, height, centerX, centerY, cornerRadius ){ - var cornerRadius = math.getRoundRectangleRadius( width, height ); + cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius; var diam = 2 * cornerRadius; // Check hBox diff --git a/src/extensions/renderer/canvas/drawing-images.js b/src/extensions/renderer/canvas/drawing-images.js index 309b60f05d..af343d53cd 100644 --- a/src/extensions/renderer/canvas/drawing-images.js +++ b/src/extensions/renderer/canvas/drawing-images.js @@ -34,6 +34,8 @@ CRp.drawInscribedImage = function( context, img, node, index, nodeOpacity ){ var shouldClip = clip === 'node'; var imgOpacity = getIndexedStyle( node, 'background-image-opacity', 'value', index ) * nodeOpacity; var smooth = getIndexedStyle( node, 'background-image-smoothing', 'value', index ); + var cornerRadius = node.pstyle('corner-radius').value + if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue var imgW = img.width || img.cachedW; var imgH = img.height || img.cachedH; @@ -152,7 +154,8 @@ CRp.drawInscribedImage = function( context, img, node, index, nodeOpacity ){ r.nodeShapes[ r.getNodeShape( node ) ].draw( context, nodeX, nodeY, - nodeTW, nodeTH ); + nodeTW, nodeTH, + cornerRadius ); context.clip(); } diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 2f4122d24b..f0374fab47 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -80,6 +80,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let outlineStyle = node.pstyle('outline-style').value; let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; let outlineOffset = node.pstyle('outline-offset').value; + let cornerRadius = node.pstyle('corner-radius').value; + if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue; context.lineJoin = 'miter'; // so borders are square with the node shape @@ -154,7 +156,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s npos.x, npos.y, nodeWidth, - nodeHeight ); + nodeHeight, cornerRadius ); } if( usePaths ){ @@ -352,13 +354,13 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s scaleY = (nodeHeight + sMult)/nodeHeight; } - r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points); + r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, cornerRadius); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { - r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); + r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) { - r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); + r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius); } else if (shape === "barrel") { r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) { diff --git a/src/extensions/renderer/canvas/drawing-shapes.js b/src/extensions/renderer/canvas/drawing-shapes.js index 45bcd375b4..3b9ada0d63 100644 --- a/src/extensions/renderer/canvas/drawing-shapes.js +++ b/src/extensions/renderer/canvas/drawing-shapes.js @@ -20,55 +20,183 @@ CRp.drawPolygonPath = function( context.closePath(); }; + +// ctx is the context to add the path to +// points is a array of points [{x :?, y: ?},... +// radius is the max rounding radius +// this creates a closed polygon. +// To draw you must call between +// ctx.beginPath(); +// roundedPoly(ctx, points, radius); +// ctx.stroke(); +// ctx.fill(); +// as it only adds a path and does not render. +// Source https://stackoverflow.com/a/44856925/11028828 +function roundedPoly(ctx, points, radiusAll) { + var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius; + // convert 2 points into vector form, polar form, and normalised + var asVec = function(p, pp, v) { + v.x = pp.x - p.x; + v.y = pp.y - p.y; + v.len = Math.sqrt(v.x * v.x + v.y * v.y); + v.nx = v.x / v.len; + v.ny = v.y / v.len; + v.ang = Math.atan2(v.ny, v.nx); + } + radius = radiusAll; + v1 = {}; + v2 = {}; + len = points.length; + p1 = points[len - 1]; + // for each point + for (i = 0; i < len; i++) { + p2 = points[(i) % len]; + p3 = points[(i + 1) % len]; + //----------------------------------------- + // Part 1 + asVec(p2, p1, v1); + asVec(p2, p3, v2); + sinA = v1.nx * v2.ny - v1.ny * v2.nx; + sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; + angle = angle = Math.asin(Math.max(-1, Math.min(1, sinA))); + //----------------------------------------- + radDirection = 1; + drawDirection = false; + if (sinA90 < 0) { + if (angle < 0) { + angle = Math.PI + angle; + } else { + angle = Math.PI - angle; + radDirection = -1; + drawDirection = true; + } + } else { + if (angle > 0) { + radDirection = -1; + drawDirection = true; + } + } + if(p2.radius !== undefined){ + radius = p2.radius; + }else{ + radius = radiusAll; + } + //----------------------------------------- + // Part 2 + halfAngle = angle / 2; + //----------------------------------------- + + //----------------------------------------- + // Part 3 + lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle)); + //----------------------------------------- + + //----------------------------------------- + // Special part A + if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { + lenOut = Math.min(v1.len / 2, v2.len / 2); + cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); + } else { + cRadius = radius; + } + //----------------------------------------- + // Part 4 + x = p2.x + v2.nx * lenOut; + y = p2.y + v2.ny * lenOut; + //----------------------------------------- + // Part 5 + x += -v2.ny * cRadius * radDirection; + y += v2.nx * cRadius * radDirection; + //----------------------------------------- + // Part 6 + ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection); + //----------------------------------------- + p1 = p2; + p2 = p3; + } + ctx.closePath(); +} + +/** + * Points in format [
+ * x_0, y_0, dx_0, dy_0,
+ * x_1, y_1, dx_1, dy_1,
+ * ...
+ * ] + */ CRp.drawRoundPolygonPath = function( - context, x, y, width, height, points ){ + context, x, y, width, height, points, radius ){ const halfW = width / 2; const halfH = height / 2; - const cornerRadius = math.getRoundPolygonRadius( width, height ); - - if( context.beginPath ){ context.beginPath(); } - - for ( let i = 0; i < points.length / 4; i++ ){ - let sourceUv, destUv; - if ( i === 0 ) { - sourceUv = points.length - 2; - } else { - sourceUv = i * 4 - 2; - } - destUv = i * 4 + 2; - - const px = x + halfW * points[ i * 4 ]; - const py = y + halfH * points[ i * 4 + 1 ]; - - - const cosTheta = (-points[ sourceUv ] * points[ destUv ] - points[ sourceUv + 1 ] * points[ destUv + 1]); - const offset = cornerRadius / Math.tan(Math.acos(cosTheta) / 2); - - const cp0x = px - offset * points[ sourceUv ]; - const cp0y = py - offset * points[ sourceUv + 1 ]; - const cp1x = px + offset * points[ destUv ]; - const cp1y = py + offset * points[ destUv + 1 ]; - - if (i === 0) { - context.moveTo( cp0x, cp0y ); - } else { - context.lineTo( cp0x, cp0y ); - } - - context.arcTo( px, py, cp1x, cp1y, cornerRadius ); - } - - context.closePath(); + const cornerRadius = radius === 'auto' ? math.getRoundPolygonRadius( width, height ) : radius; + // console.log(points) + const p = new Array(points.length / 4) + for ( let i = 0; i < points.length / 4; i++ ){ + p[i] = {x: x + halfW * points[i*4], y: y + halfH * points[i*4+1]} + } + // + roundedPoly(context, p, cornerRadius) + // if( context.beginPath ){ context.beginPath(); } + // + // for ( let i = 0; i < points.length / 4; i++ ){ + // let sourceUv, destUv; + // if ( i === 0 ) { + // sourceUv = points.length - 2; + // } else { + // sourceUv = i * 4 - 2; + // } + // destUv = i * 4 + 2; + // + // const p = { + // x: x + halfW * points[ i * 4 ], + // y: y + halfH * points[ i * 4 + 1 ] + // }; + // const source = { + // x: x + halfW * points[ sourceUv - 2 ], + // y: y + halfH * points[ sourceUv - 1 ] + // }; + // const dest = { + // x: x + halfW * points[ (destUv + 2) % points.length], + // y: y + halfH * points[ (destUv + 3) % points.length] + // }; + // + // const r = Math.min( + // Math.min(math.dist(p, source), math.dist(p, dest)) / 2, + // cornerRadius + // ); + // // console.log({p, source, dest, + // // dS: math.dist(source, p) / 2, + // // dD: math.dist(p, dest) / 2, + // // cornerRadius, r}) + // + // + // const cosTheta = (-points[ sourceUv ] * points[ destUv ] - points[ sourceUv + 1 ] * points[ destUv + 1]); + // const offset = r / Math.tan(Math.acos(cosTheta) / 2) ; + // + // const cp0x = p.x - offset * points[ sourceUv ]; + // const cp0y = p.y - offset * points[ sourceUv + 1 ]; + // const cp1x = p.x + offset * points[ destUv ]; + // const cp1y = p.y + offset * points[ destUv + 1 ]; + // + // if (i === 0) { + // context.moveTo( cp0x, cp0y ); + // } else { + // context.lineTo( cp0x, cp0y ); + // } + // + // context.arcTo( p.x, p.y, cp1x, cp1y, r ); + // } + // context.closePath(); }; // Round rectangle drawing CRp.drawRoundRectanglePath = function( - context, x, y, width, height ){ + context, x, y, width, height, radius){ var halfWidth = width / 2; var halfHeight = height / 2; - var cornerRadius = math.getRoundRectangleRadius( width, height ); + var cornerRadius = radius === 'auto' ? math.getRoundRectangleRadius( width, height ) : Math.min(radius, halfHeight, halfWidth); if( context.beginPath ){ context.beginPath(); } @@ -90,11 +218,11 @@ CRp.drawRoundRectanglePath = function( }; CRp.drawBottomRoundRectanglePath = function( - context, x, y, width, height ){ + context, x, y, width, height, radius){ var halfWidth = width / 2; var halfHeight = height / 2; - var cornerRadius = math.getRoundRectangleRadius( width, height ); + var cornerRadius = radius === 'auto' ? math.getRoundRectangleRadius( width, height ) : radius; if( context.beginPath ){ context.beginPath(); } diff --git a/src/extensions/renderer/canvas/node-shapes.js b/src/extensions/renderer/canvas/node-shapes.js index ce14610aa6..7a82b1b2a0 100644 --- a/src/extensions/renderer/canvas/node-shapes.js +++ b/src/extensions/renderer/canvas/node-shapes.js @@ -1,22 +1,22 @@ var CRp = {}; -CRp.nodeShapeImpl = function( name, context, centerX, centerY, width, height, points ){ +CRp.nodeShapeImpl = function( name, context, centerX, centerY, width, height, points, cornerRadius ){ switch( name ){ case 'ellipse': return this.drawEllipsePath( context, centerX, centerY, width, height ); case 'polygon': return this.drawPolygonPath( context, centerX, centerY, width, height, points ); case 'round-polygon': - return this.drawRoundPolygonPath(context, centerX, centerY, width, height, points ); + return this.drawRoundPolygonPath(context, centerX, centerY, width, height, points, cornerRadius ); case 'roundrectangle': case 'round-rectangle': - return this.drawRoundRectanglePath( context, centerX, centerY, width, height ); + return this.drawRoundRectanglePath( context, centerX, centerY, width, height, points, cornerRadius ); case 'cutrectangle': case 'cut-rectangle': return this.drawCutRectanglePath( context, centerX, centerY, width, height ); case 'bottomroundrectangle': case 'bottom-round-rectangle': - return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height ); + return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height, cornerRadius ); case 'barrel': return this.drawBarrelPath( context, centerX, centerY, width, height ); } diff --git a/src/math.js b/src/math.js index bb9e4b614d..2839642bda 100644 --- a/src/math.js +++ b/src/math.js @@ -371,9 +371,9 @@ export const boundingBoxInBoundingBox = ( bb1, bb2 ) => ( && inBoundingBox( bb1, bb2.x2, bb2.y2 ) ); -export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, padding ) => { +export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, padding, radius = 'auto' ) => { - let cornerRadius = getRoundRectangleRadius( width, height ); + let cornerRadius = radius === 'auto' ? getRoundRectangleRadius( width, height ) : radius; let halfWidth = width / 2; let halfHeight = height / 2; @@ -808,11 +808,11 @@ export const pointInsidePolygon = ( x, y, basePoints, centerX, centerY, width, h return pointInsidePolygonPoints( x, y, points ); }; -export const pointInsideRoundPolygon = ( x, y, basePoints, centerX, centerY, width, height ) => { +export const pointInsideRoundPolygon = ( x, y, basePoints, centerX, centerY, width, height, radius = 'auto' ) => { const cutPolygonPoints = new Array( basePoints.length); const halfW = width / 2; const halfH = height / 2; - const cornerRadius = getRoundPolygonRadius(width, height); + const cornerRadius = radius === 'auto' ? getRoundPolygonRadius(width, height) : radius; const squaredCornerRadius = cornerRadius * cornerRadius; for ( let i = 0; i < basePoints.length / 4; i++ ){ @@ -1197,13 +1197,13 @@ export const polygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, return intersections; }; -export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding ) => { +export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, radius = 'auto' ) => { let intersections = []; let intersection; let lines = new Array(basePoints.length); const halfW = width / 2; const halfH = height / 2; - const cornerRadius = getRoundPolygonRadius(width, height); + const cornerRadius = radius === 'auto' ? getRoundPolygonRadius(width, height) : radius; for ( let i = 0; i < basePoints.length / 4; i++ ){ let sourceUv, destUv; diff --git a/src/style/properties.js b/src/style/properties.js index 029e8c9f5a..3b3e0d30da 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -73,6 +73,7 @@ const styfn = {}; 'tag', 'round-tag', 'star', 'diamond', 'round-diamond', 'vee', 'rhomboid', 'right-rhomboid', 'polygon', ] }, overlayShape: { enums: [ 'roundrectangle', 'round-rectangle', 'ellipse' ] }, + cornerRadius: { number: true, min: 0, units: '%|px|em', implicitUnits: 'px', enums: ['auto'] }, compoundIncludeLabels: { enums: [ 'include', 'exclude' ] }, arrowShape: { enums: [ 'tee', 'triangle', 'triangle-tee', 'circle-triangle', 'triangle-cross', 'triangle-backcurve', 'vee', 'square', 'circle', 'diamond', 'chevron', 'none' ] }, arrowFill: { enums: [ 'filled', 'hollow' ] }, @@ -280,6 +281,7 @@ const styfn = {}; { name: 'width', type: t.nodeSize, triggersBounds: diff.any, hashOverride: nodeSizeHashOverride }, { name: 'shape', type: t.nodeShape, triggersBounds: diff.any }, { name: 'shape-polygon-points', type: t.polygonPointList, triggersBounds: diff.any }, + { name: 'corner-radius', type: t.cornerRadius}, { name: 'background-color', type: t.color }, { name: 'background-fill', type: t.fill }, { name: 'background-opacity', type: t.zeroOneNumber }, @@ -644,6 +646,7 @@ styfn.getDefaultProperties = function(){ 'width': 30, 'shape': 'ellipse', 'shape-polygon-points': '-1, -1, 1, -1, 1, 1, -1, 1', + 'corner-radius': 'auto', 'bounds-expansion': 0, // node gradient From 620b277b5f3d7ce2db8f9a79ca71462e5b1e5506 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 15 Dec 2023 13:56:42 +0000 Subject: [PATCH 02/21] :construction: WIP --- debug/init.js | 13 +- .../renderer/canvas/drawing-edges.js | 13 +- .../renderer/canvas/drawing-shapes.js | 152 +++++++++--------- 3 files changed, 96 insertions(+), 82 deletions(-) diff --git a/debug/init.js b/debug/init.js index c9f8957459..bebcfecbdf 100644 --- a/debug/init.js +++ b/debug/init.js @@ -29,10 +29,11 @@ var cy, defaultSty, options; 'target-arrow-shape': 'triangle', 'mid-target-arrow-shape': 'triangle', 'mid-source-arrow-shape': 'triangle-backcurve', + 'curve-style': 'taxi', }) .selector('#ab') .style({ - 'curve-style': 'unbundled-bezier', + // 'curve-style': 'unbundled-bezier', 'control-point-distances': [ 20, -100, 20 ], 'control-point-weights': [ 0.25, 0.5, 0.75 ], 'source-arrow-fill': 'hollow', @@ -42,7 +43,7 @@ var cy, defaultSty, options; }) .selector('#bc') .style({ - 'curve-style': 'segments', + // 'curve-style': 'segments', 'segment-distances': [ 20, -80 ], 'segment-weights': [ 0.25, 0.5 ], 'source-arrow-fill': 'hollow', @@ -51,7 +52,7 @@ var cy, defaultSty, options; }) .selector('#ef') .style({ - 'curve-style': 'straight-triangle', + // 'curve-style': 'straight-triangle', 'source-arrow-shape': 'none', 'target-arrow-shape': 'none', 'mid-target-arrow-shape': 'none', @@ -60,16 +61,16 @@ var cy, defaultSty, options; }) .selector('[source = "c"][target = "e"]') .style({ - 'curve-style': 'haystack', + // 'curve-style': 'haystack', 'haystack-radius': 0.5 }) .selector('[source = "d"][target = "e"]') .style({ - 'curve-style': 'bezier' + // 'curve-style': 'bezier' }) .selector('[source = "b"][target = "f"]') .style({ - 'curve-style': 'taxi' + // 'curve-style': 'taxi' }) ; diff --git a/src/extensions/renderer/canvas/drawing-edges.js b/src/extensions/renderer/canvas/drawing-edges.js index 19d1037cc3..d25270dda8 100644 --- a/src/extensions/renderer/canvas/drawing-edges.js +++ b/src/extensions/renderer/canvas/drawing-edges.js @@ -1,6 +1,7 @@ /* global Path2D */ import * as util from '../../../util'; +import {drawRoundCorner} from "./drawing-shapes"; let CRp = {}; @@ -206,12 +207,20 @@ CRp.drawEdgePath = function( edge, context, pts, type ){ break; case 'straight': - case 'segments': case 'haystack': - for( let i = 2; i + 1 < pts.length; i += 2 ){ + for( let i = 2; i + 1 < pts.length; i += 2 ) { context.lineTo( pts[ i ], pts[ i + 1] ); } break; + case 'segments': + for( let i = 2; i + 3 < pts.length; i += 2 ){ + drawRoundCorner(context, + {x: pts[i - 2], y: pts[i - 1]}, + {x: pts[i], y: pts[i + 1]}, + {x: pts[i + 2], y: pts[i + 3]}, 20000) + } + context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] ); + break; } } diff --git a/src/extensions/renderer/canvas/drawing-shapes.js b/src/extensions/renderer/canvas/drawing-shapes.js index 3b9ada0d63..3119c79d41 100644 --- a/src/extensions/renderer/canvas/drawing-shapes.js +++ b/src/extensions/renderer/canvas/drawing-shapes.js @@ -21,6 +21,81 @@ CRp.drawPolygonPath = function( }; +// convert 2 points into vector form, polar form, and normalised +const asVec = function(p, pp, v) { + v.x = pp.x - p.x; + v.y = pp.y - p.y; + v.len = Math.sqrt(v.x * v.x + v.y * v.y); + v.nx = v.x / v.len; + v.ny = v.y / v.len; + v.ang = Math.atan2(v.ny, v.nx); +} +export function drawRoundCorner(ctx, previousPoint, currentPoint, nextPoint, radiusAll) { + let x, y, v1 = {}, v2 = {}, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut, radius; + + v1 = {}; + v2 = {}; + //----------------------------------------- + // Part 1 + asVec(currentPoint, previousPoint, v1); + asVec(currentPoint, nextPoint, v2); + sinA = v1.nx * v2.ny - v1.ny * v2.nx; + sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; + angle = angle = Math.asin(Math.max(-1, Math.min(1, sinA))); + //----------------------------------------- + radDirection = 1; + drawDirection = false; + if (sinA90 < 0) { + if (angle < 0) { + angle = Math.PI + angle; + } else { + angle = Math.PI - angle; + radDirection = -1; + drawDirection = true; + } + } else { + if (angle > 0) { + radDirection = -1; + drawDirection = true; + } + } + if (currentPoint.radius !== undefined) { + radius = currentPoint.radius; + } else { + radius = radiusAll; + } + //----------------------------------------- + // Part 2 + halfAngle = angle / 2; + //----------------------------------------- + + //----------------------------------------- + // Part 3 + lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle)); + //----------------------------------------- + + //----------------------------------------- + // Special part A + if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { + lenOut = Math.min(v1.len / 2, v2.len / 2); + cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); + } else { + cRadius = radius; + } + //----------------------------------------- + // Part 4 + x = currentPoint.x + v2.nx * lenOut; + y = currentPoint.y + v2.ny * lenOut; + //----------------------------------------- + // Part 5 + x += -v2.ny * cRadius * radDirection; + y += v2.nx * cRadius * radDirection; + //----------------------------------------- + // Part 6 + ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection); + //----------------------------------------- +} + // ctx is the context to add the path to // points is a array of points [{x :?, y: ?},... // radius is the max rounding radius @@ -33,84 +108,14 @@ CRp.drawPolygonPath = function( // as it only adds a path and does not render. // Source https://stackoverflow.com/a/44856925/11028828 function roundedPoly(ctx, points, radiusAll) { - var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius; - // convert 2 points into vector form, polar form, and normalised - var asVec = function(p, pp, v) { - v.x = pp.x - p.x; - v.y = pp.y - p.y; - v.len = Math.sqrt(v.x * v.x + v.y * v.y); - v.nx = v.x / v.len; - v.ny = v.y / v.len; - v.ang = Math.atan2(v.ny, v.nx); - } - radius = radiusAll; - v1 = {}; - v2 = {}; - len = points.length; + let i, len = points.length, p1, p2, p3; + p1 = points[len - 1]; // for each point for (i = 0; i < len; i++) { p2 = points[(i) % len]; p3 = points[(i + 1) % len]; - //----------------------------------------- - // Part 1 - asVec(p2, p1, v1); - asVec(p2, p3, v2); - sinA = v1.nx * v2.ny - v1.ny * v2.nx; - sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; - angle = angle = Math.asin(Math.max(-1, Math.min(1, sinA))); - //----------------------------------------- - radDirection = 1; - drawDirection = false; - if (sinA90 < 0) { - if (angle < 0) { - angle = Math.PI + angle; - } else { - angle = Math.PI - angle; - radDirection = -1; - drawDirection = true; - } - } else { - if (angle > 0) { - radDirection = -1; - drawDirection = true; - } - } - if(p2.radius !== undefined){ - radius = p2.radius; - }else{ - radius = radiusAll; - } - //----------------------------------------- - // Part 2 - halfAngle = angle / 2; - //----------------------------------------- - - //----------------------------------------- - // Part 3 - lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle)); - //----------------------------------------- - - //----------------------------------------- - // Special part A - if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { - lenOut = Math.min(v1.len / 2, v2.len / 2); - cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); - } else { - cRadius = radius; - } - //----------------------------------------- - // Part 4 - x = p2.x + v2.nx * lenOut; - y = p2.y + v2.ny * lenOut; - //----------------------------------------- - // Part 5 - x += -v2.ny * cRadius * radDirection; - y += v2.nx * cRadius * radDirection; - //----------------------------------------- - // Part 6 - ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection); - //----------------------------------------- + drawRoundCorner(ctx, p1, p2, p3, radiusAll); p1 = p2; p2 = p3; } @@ -130,7 +135,6 @@ CRp.drawRoundPolygonPath = function( const halfW = width / 2; const halfH = height / 2; const cornerRadius = radius === 'auto' ? math.getRoundPolygonRadius( width, height ) : radius; - // console.log(points) const p = new Array(points.length / 4) for ( let i = 0; i < points.length / 4; i++ ){ p[i] = {x: x + halfW * points[i*4], y: y + halfH * points[i*4+1]} From 79ce93e19f73f988e2554aa7d14f7bef622082ea Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 15 Dec 2023 21:08:56 +0000 Subject: [PATCH 03/21] :sparkles: Support rounded rectangle up to pill shape --- debug/init.js | 19 +++++++++--------- .../coord-ele-math/edge-control-points.js | 13 +++++++----- .../base/coord-ele-math/edge-endpoints.js | 2 +- src/extensions/renderer/base/node-shapes.js | 20 ++++++++++--------- src/math.js | 10 ++++++---- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/debug/init.js b/debug/init.js index bebcfecbdf..5904b838ed 100644 --- a/debug/init.js +++ b/debug/init.js @@ -13,8 +13,9 @@ var cy, defaultSty, options; .selector('node#a') .style({ 'shape': 'round-rectangle', - 'width': 35, - 'corner-radius': 200 + 'width': 100, + 'height': 50, + 'corner-radius': 25 }) .selector('node#b') .style({ @@ -29,11 +30,11 @@ var cy, defaultSty, options; 'target-arrow-shape': 'triangle', 'mid-target-arrow-shape': 'triangle', 'mid-source-arrow-shape': 'triangle-backcurve', - 'curve-style': 'taxi', + 'curve-style': 'straight', }) .selector('#ab') .style({ - // 'curve-style': 'unbundled-bezier', + 'curve-style': 'unbundled-bezier', 'control-point-distances': [ 20, -100, 20 ], 'control-point-weights': [ 0.25, 0.5, 0.75 ], 'source-arrow-fill': 'hollow', @@ -43,7 +44,7 @@ var cy, defaultSty, options; }) .selector('#bc') .style({ - // 'curve-style': 'segments', + 'curve-style': 'segments', 'segment-distances': [ 20, -80 ], 'segment-weights': [ 0.25, 0.5 ], 'source-arrow-fill': 'hollow', @@ -52,7 +53,7 @@ var cy, defaultSty, options; }) .selector('#ef') .style({ - // 'curve-style': 'straight-triangle', + 'curve-style': 'straight-triangle', 'source-arrow-shape': 'none', 'target-arrow-shape': 'none', 'mid-target-arrow-shape': 'none', @@ -61,16 +62,16 @@ var cy, defaultSty, options; }) .selector('[source = "c"][target = "e"]') .style({ - // 'curve-style': 'haystack', + 'curve-style': 'haystack', 'haystack-radius': 0.5 }) .selector('[source = "d"][target = "e"]') .style({ - // 'curve-style': 'bezier' + 'curve-style': 'bezier' }) .selector('[source = "b"][target = "f"]') .style({ - // 'curve-style': 'taxi' + 'curve-style': 'taxi' }) ; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js index da75bd91f7..5e08923033 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js @@ -463,7 +463,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ // can only correct beziers for now... if( rs.edgeType === 'bezier' ){ - const { srcPos, tgtPos, srcW, srcH, tgtW, tgtH, srcShape, tgtShape } = pairInfo; + const { srcPos, tgtPos, srcW, srcH, tgtW, tgtH, srcShape, tgtShape, srcCornerRadius, tgtCornerRadius } = pairInfo; let badStart = !is.number( rs.startX ) || !is.number( rs.startY ); let badAStart = !is.number( rs.arrowStartX ) || !is.number( rs.arrowStartY ); @@ -509,7 +509,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ srcH, cpProj.x, cpProj.y, - 0 + 0, srcCornerRadius ); if( closeStartACp ){ @@ -548,7 +548,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ tgtH, cpProj.x, cpProj.y, - 0 + 0, tgtCornerRadius ); if( closeEndACp ){ @@ -767,6 +767,9 @@ BRp.findEdgeControlPoints = function( edges ){ let srcShape = pairInfo.srcShape = r.nodeShapes[ this.getNodeShape( src ) ]; let tgtShape = pairInfo.tgtShape = r.nodeShapes[ this.getNodeShape( tgt ) ]; + let srcCornerRadius = pairInfo.srcCornerRadius = src.pstyle('corner-radius').value === 'auto' ? 'auto' : src.pstyle('corner-radius').pfValue; + let tgtCornerRadius = pairInfo.tgtCornerRadius = tgt.pstyle('corner-radius').value === 'auto' ? 'auto' : tgt.pstyle('corner-radius').pfValue; + pairInfo.dirCounts = { 'north': 0, 'west': 0, @@ -795,7 +798,7 @@ BRp.findEdgeControlPoints = function( edges ){ srcPos.x, srcPos.y, srcW, srcH, tgtPos.x, tgtPos.y, - 0 + 0, srcCornerRadius ); let srcIntn = pairInfo.srcIntn = srcOutside; @@ -805,7 +808,7 @@ BRp.findEdgeControlPoints = function( edges ){ tgtPos.x, tgtPos.y, tgtW, tgtH, srcPos.x, srcPos.y, - 0 + 0, tgtCornerRadius ); let tgtIntn = pairInfo.tgtIntn = tgtOutside; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js index 560a5f7561..b427536d46 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js @@ -43,7 +43,7 @@ BRp.manualEndptToPx = function( node, prop ){ npos.x, npos.y, w, h, p[0], p[1], - 0 + 0, node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue ); } }; diff --git a/src/extensions/renderer/base/node-shapes.js b/src/extensions/renderer/base/node-shapes.js index 007a976389..db3bb17cc0 100644 --- a/src/extensions/renderer/base/node-shapes.js +++ b/src/extensions/renderer/base/node-shapes.js @@ -142,8 +142,10 @@ BRp.generateRoundRectangle = function(){ checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ - + let halfWidth = width / 2; + let halfHeight = height / 2; cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius; + cornerRadius = Math.min(halfWidth, halfHeight, cornerRadius) var diam = cornerRadius * 2; // Check hBox @@ -161,8 +163,8 @@ BRp.generateRoundRectangle = function(){ // Check top left quarter circle if( math.checkInEllipse( x, y, diam, diam, - centerX - width / 2 + cornerRadius, - centerY - height / 2 + cornerRadius, + centerX - halfWidth + cornerRadius, + centerY - halfHeight + cornerRadius, padding ) ){ return true; @@ -171,8 +173,8 @@ BRp.generateRoundRectangle = function(){ // Check top right quarter circle if( math.checkInEllipse( x, y, diam, diam, - centerX + width / 2 - cornerRadius, - centerY - height / 2 + cornerRadius, + centerX + halfWidth - cornerRadius, + centerY - halfHeight + cornerRadius, padding ) ){ return true; @@ -181,8 +183,8 @@ BRp.generateRoundRectangle = function(){ // Check bottom right quarter circle if( math.checkInEllipse( x, y, diam, diam, - centerX + width / 2 - cornerRadius, - centerY + height / 2 - cornerRadius, + centerX + halfWidth - cornerRadius, + centerY + halfHeight - cornerRadius, padding ) ){ return true; @@ -191,8 +193,8 @@ BRp.generateRoundRectangle = function(){ // Check bottom left quarter circle if( math.checkInEllipse( x, y, diam, diam, - centerX - width / 2 + cornerRadius, - centerY + height / 2 - cornerRadius, + centerX - halfWidth + cornerRadius, + centerY + halfHeight - cornerRadius, padding ) ){ return true; diff --git a/src/math.js b/src/math.js index 2839642bda..af5b57fdb8 100644 --- a/src/math.js +++ b/src/math.js @@ -377,12 +377,14 @@ export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, let halfWidth = width / 2; let halfHeight = height / 2; + cornerRadius = Math.min(cornerRadius, halfWidth, halfHeight); + const doWidth = cornerRadius !== halfWidth, doHeight = cornerRadius !== halfHeight; // Check intersections with straight line segments let straightLineIntersections; // Top segment, left to right - { + if( doWidth ){ let topStartX = nodeX - halfWidth + cornerRadius - padding; let topStartY = nodeY - halfHeight - padding; let topEndX = nodeX + halfWidth - cornerRadius + padding; @@ -397,7 +399,7 @@ export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, } // Right segment, top to bottom - { + if( doHeight ){ let rightStartX = nodeX + halfWidth + padding; let rightStartY = nodeY - halfHeight + cornerRadius - padding; let rightEndX = rightStartX; @@ -412,7 +414,7 @@ export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, } // Bottom segment, left to right - { + if( doWidth ){ let bottomStartX = nodeX - halfWidth + cornerRadius - padding; let bottomStartY = nodeY + halfHeight + padding; let bottomEndX = nodeX + halfWidth - cornerRadius + padding; @@ -427,7 +429,7 @@ export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, } // Left segment, top to bottom - { + if( doHeight ){ let leftStartX = nodeX - halfWidth - padding; let leftStartY = nodeY - halfHeight + cornerRadius - padding; let leftEndX = leftStartX; From 6514eb33585b995507d87fe32271042014095d64 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 15 Dec 2023 21:10:26 +0000 Subject: [PATCH 04/21] :sparkles: Support rounded polygons (path + intersect + checkPoint) --- src/extensions/renderer/base/node-shapes.js | 29 +-- .../renderer/canvas/drawing-shapes.js | 191 +++--------------- src/math.js | 134 +++++------- src/round.js | 153 ++++++++++++++ 4 files changed, 234 insertions(+), 273 deletions(-) create mode 100644 src/round.js diff --git a/src/extensions/renderer/base/node-shapes.js b/src/extensions/renderer/base/node-shapes.js index db3bb17cc0..a16710cd4b 100644 --- a/src/extensions/renderer/base/node-shapes.js +++ b/src/extensions/renderer/base/node-shapes.js @@ -61,39 +61,12 @@ BRp.generateEllipse = function(){ BRp.generateRoundPolygon = function( name, points ){ - // Pre-compute control points - // Since these points depend on the radius length (which in turns depend on the width/height of the node) we will only pre-compute - // the unit vectors. - // For simplicity the layout will be: - // [ p0, UnitVectorP0P1, p1, UniVectorP1P2, ..., pn, UnitVectorPnP0 ] - const allPoints = new Array( points.length * 2 ); - - for ( let i = 0; i < points.length / 2; i++ ){ - const sourceIndex = i * 2; - let destIndex; - if (i < points.length / 2 - 1) { - destIndex = (i + 1) * 2; - } else { - destIndex = 0; - } - - allPoints[ i * 4 ] = points[ sourceIndex ]; - allPoints[ i * 4 + 1 ] = points[ sourceIndex + 1 ]; - - const xDest = points[ destIndex ] - points[ sourceIndex ]; - const yDest = points[ destIndex + 1] - points[ sourceIndex + 1 ]; - const norm = Math.sqrt(xDest * xDest + yDest * yDest); - - allPoints[ i * 4 + 2 ] = xDest / norm; - allPoints[ i * 4 + 3 ] = yDest / norm; - } - return ( this.nodeShapes[ name ] = { renderer: this, name: name, - points: allPoints, + points: points, draw: function( context, centerX, centerY, width, height, cornerRadius ){ this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points, cornerRadius ); diff --git a/src/extensions/renderer/canvas/drawing-shapes.js b/src/extensions/renderer/canvas/drawing-shapes.js index 3119c79d41..a70daa7736 100644 --- a/src/extensions/renderer/canvas/drawing-shapes.js +++ b/src/extensions/renderer/canvas/drawing-shapes.js @@ -1,4 +1,5 @@ import * as math from '../../../math'; +import * as round from '../../../round'; var CRp = {}; @@ -20,178 +21,36 @@ CRp.drawPolygonPath = function( context.closePath(); }; - -// convert 2 points into vector form, polar form, and normalised -const asVec = function(p, pp, v) { - v.x = pp.x - p.x; - v.y = pp.y - p.y; - v.len = Math.sqrt(v.x * v.x + v.y * v.y); - v.nx = v.x / v.len; - v.ny = v.y / v.len; - v.ang = Math.atan2(v.ny, v.nx); -} -export function drawRoundCorner(ctx, previousPoint, currentPoint, nextPoint, radiusAll) { - let x, y, v1 = {}, v2 = {}, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut, radius; - - v1 = {}; - v2 = {}; - //----------------------------------------- - // Part 1 - asVec(currentPoint, previousPoint, v1); - asVec(currentPoint, nextPoint, v2); - sinA = v1.nx * v2.ny - v1.ny * v2.nx; - sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; - angle = angle = Math.asin(Math.max(-1, Math.min(1, sinA))); - //----------------------------------------- - radDirection = 1; - drawDirection = false; - if (sinA90 < 0) { - if (angle < 0) { - angle = Math.PI + angle; - } else { - angle = Math.PI - angle; - radDirection = -1; - drawDirection = true; - } - } else { - if (angle > 0) { - radDirection = -1; - drawDirection = true; - } - } - if (currentPoint.radius !== undefined) { - radius = currentPoint.radius; - } else { - radius = radiusAll; - } - //----------------------------------------- - // Part 2 - halfAngle = angle / 2; - //----------------------------------------- - - //----------------------------------------- - // Part 3 - lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle)); - //----------------------------------------- - - //----------------------------------------- - // Special part A - if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { - lenOut = Math.min(v1.len / 2, v2.len / 2); - cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); - } else { - cRadius = radius; - } - //----------------------------------------- - // Part 4 - x = currentPoint.x + v2.nx * lenOut; - y = currentPoint.y + v2.ny * lenOut; - //----------------------------------------- - // Part 5 - x += -v2.ny * cRadius * radDirection; - y += v2.nx * cRadius * radDirection; - //----------------------------------------- - // Part 6 - ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection); - //----------------------------------------- -} - -// ctx is the context to add the path to -// points is a array of points [{x :?, y: ?},... -// radius is the max rounding radius -// this creates a closed polygon. -// To draw you must call between -// ctx.beginPath(); -// roundedPoly(ctx, points, radius); -// ctx.stroke(); -// ctx.fill(); -// as it only adds a path and does not render. -// Source https://stackoverflow.com/a/44856925/11028828 -function roundedPoly(ctx, points, radiusAll) { - let i, len = points.length, p1, p2, p3; - - p1 = points[len - 1]; - // for each point - for (i = 0; i < len; i++) { - p2 = points[(i) % len]; - p3 = points[(i + 1) % len]; - drawRoundCorner(ctx, p1, p2, p3, radiusAll); - p1 = p2; - p2 = p3; - } - ctx.closePath(); -} - -/** - * Points in format [
- * x_0, y_0, dx_0, dy_0,
- * x_1, y_1, dx_1, dy_1,
- * ...
- * ] - */ CRp.drawRoundPolygonPath = function( context, x, y, width, height, points, radius ){ const halfW = width / 2; const halfH = height / 2; const cornerRadius = radius === 'auto' ? math.getRoundPolygonRadius( width, height ) : radius; - const p = new Array(points.length / 4) - for ( let i = 0; i < points.length / 4; i++ ){ - p[i] = {x: x + halfW * points[i*4], y: y + halfH * points[i*4+1]} - } - // - roundedPoly(context, p, cornerRadius) - // if( context.beginPath ){ context.beginPath(); } - // - // for ( let i = 0; i < points.length / 4; i++ ){ - // let sourceUv, destUv; - // if ( i === 0 ) { - // sourceUv = points.length - 2; - // } else { - // sourceUv = i * 4 - 2; - // } - // destUv = i * 4 + 2; - // - // const p = { - // x: x + halfW * points[ i * 4 ], - // y: y + halfH * points[ i * 4 + 1 ] - // }; - // const source = { - // x: x + halfW * points[ sourceUv - 2 ], - // y: y + halfH * points[ sourceUv - 1 ] - // }; - // const dest = { - // x: x + halfW * points[ (destUv + 2) % points.length], - // y: y + halfH * points[ (destUv + 3) % points.length] - // }; - // - // const r = Math.min( - // Math.min(math.dist(p, source), math.dist(p, dest)) / 2, - // cornerRadius - // ); - // // console.log({p, source, dest, - // // dS: math.dist(source, p) / 2, - // // dD: math.dist(p, dest) / 2, - // // cornerRadius, r}) - // - // - // const cosTheta = (-points[ sourceUv ] * points[ destUv ] - points[ sourceUv + 1 ] * points[ destUv + 1]); - // const offset = r / Math.tan(Math.acos(cosTheta) / 2) ; - // - // const cp0x = p.x - offset * points[ sourceUv ]; - // const cp0y = p.y - offset * points[ sourceUv + 1 ]; - // const cp1x = p.x + offset * points[ destUv ]; - // const cp1y = p.y + offset * points[ destUv + 1 ]; - // - // if (i === 0) { - // context.moveTo( cp0x, cp0y ); - // } else { - // context.lineTo( cp0x, cp0y ); - // } - // - // context.arcTo( p.x, p.y, cp1x, cp1y, r ); - // } - // context.closePath(); + const p = new Array( points.length / 2 ); + + for ( let i = 0; i < points.length / 2; i++ ){ + p[i] = { + x: x + halfW * points[ i * 2 ], + y: y + halfH * points[ i * 2 + 1 ] + } + } + + let i, p1, p2, p3, len = p.length; + + p1 = p[ len - 1 ]; + // for each point + for( i = 0; i < len; i++ ){ + p2 = p[ (i) % len ]; + p3 = p[ (i + 1) % len ]; + + let corner = round.getRoundCorner( p1, p2, p3, cornerRadius ); + round.drawPreparedRoundCorner( context, corner ); + + p1 = p2; + p2 = p3; + } + context.closePath(); }; // Round rectangle drawing diff --git a/src/math.js b/src/math.js index af5b57fdb8..566b854528 100644 --- a/src/math.js +++ b/src/math.js @@ -1,3 +1,5 @@ +import * as round from "./round"; + export const arePositionsSame = ( p1, p2 ) => p1.x === p2.x && p1.y === p2.y; @@ -810,51 +812,41 @@ export const pointInsidePolygon = ( x, y, basePoints, centerX, centerY, width, h return pointInsidePolygonPoints( x, y, points ); }; -export const pointInsideRoundPolygon = ( x, y, basePoints, centerX, centerY, width, height, radius = 'auto' ) => { - const cutPolygonPoints = new Array( basePoints.length); +export const pointInsideRoundPolygon = (x, y, basePoints, centerX, centerY, width, height, radius = 'auto') => { + const cutPolygonPoints = new Array( basePoints.length * 2 ); const halfW = width / 2; const halfH = height / 2; - const cornerRadius = radius === 'auto' ? getRoundPolygonRadius(width, height) : radius; - const squaredCornerRadius = cornerRadius * cornerRadius; + const cornerRadius = radius === 'auto' ? getRoundPolygonRadius( width, height ) : radius; + const p = new Array( basePoints.length / 2 ); - for ( let i = 0; i < basePoints.length / 4; i++ ){ - let sourceUv, destUv; - if ( i === 0 ) { - sourceUv = basePoints.length - 2; - } else { - sourceUv = i * 4 - 2; - } - destUv = i * 4 + 2; - - const px = centerX + halfW * basePoints[ i * 4 ]; - const py = centerY + halfH * basePoints[ i * 4 + 1 ]; - const cosTheta = (-basePoints[ sourceUv ] * basePoints[ destUv ] - basePoints[ sourceUv + 1 ] * basePoints[ destUv + 1]); - const offset = cornerRadius / Math.tan(Math.acos(cosTheta) / 2); - - const cp0x = px - offset * basePoints[ sourceUv ]; - const cp0y = py - offset * basePoints[ sourceUv + 1 ]; - const cp1x = px + offset * basePoints[ destUv ]; - const cp1y = py + offset * basePoints[ destUv + 1 ]; - cutPolygonPoints[ i * 4] = cp0x; - cutPolygonPoints[ i * 4 + 1] = cp0y; - cutPolygonPoints[ i * 4 + 2] = cp1x; - cutPolygonPoints[ i * 4 + 3] = cp1y; - - let orthx = basePoints[ sourceUv + 1 ]; - let orthy = -basePoints[ sourceUv ]; - const cosAlpha = orthx * basePoints[ destUv ] + orthy * basePoints [ destUv + 1 ]; - if (cosAlpha < 0) { - orthx *= -1; - orthy *= -1; - } + for( let i = 0; 2 * i + 1 < basePoints.length; i++ ){ + p[ i ] = { + x: x + halfW * basePoints[ 2 * i ], + y: y + halfH * basePoints[ 2 * i + 1 ] + }; + } + + let i, p1, p2, p3, len = p.length; + + p1 = p[len - 1]; + // for each point + for (i = 0; i < len; i++) { + p2 = p[(i) % len]; + p3 = p[(i + 1) % len]; + let corner = round.getRoundCorner(p1, p2, p3, cornerRadius); - const cx = cp0x + orthx * cornerRadius; - const cy = cp0y + orthy * cornerRadius; + cutPolygonPoints[i * 4 + 0] = corner.startX; + cutPolygonPoints[i * 4 + 1] = corner.startY; + cutPolygonPoints[i * 4 + 2] = corner.stopX; + cutPolygonPoints[i * 4 + 3] = corner.stopY; - const squaredDistance = Math.pow(cx - x, 2) + Math.pow(cy - y, 2); - if (squaredDistance <= squaredCornerRadius) { + const squaredDistance = Math.pow(corner.cx - x, 2) + Math.pow(corner.cy - y, 2); + if (squaredDistance <= Math.pow(corner.radius, 2)) { return true; } + + p1 = p2; + p2 = p3; } return pointInsidePolygonPoints(x, y, cutPolygonPoints); @@ -1199,62 +1191,46 @@ export const polygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, return intersections; }; -export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, radius = 'auto' ) => { +export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, radius ) => { let intersections = []; let intersection; - let lines = new Array(basePoints.length); + let lines = new Array(basePoints.length * 2); const halfW = width / 2; const halfH = height / 2; const cornerRadius = radius === 'auto' ? getRoundPolygonRadius(width, height) : radius; + const p = new Array(basePoints.length / 2); - for ( let i = 0; i < basePoints.length / 4; i++ ){ - let sourceUv, destUv; - if ( i === 0 ) { - sourceUv = basePoints.length - 2; - } else { - sourceUv = i * 4 - 2; - } - destUv = i * 4 + 2; - - const px = centerX + halfW * basePoints[ i * 4 ]; - const py = centerY + halfH * basePoints[ i * 4 + 1 ]; - - - const cosTheta = (-basePoints[ sourceUv ] * basePoints[ destUv ] - basePoints[ sourceUv + 1 ] * basePoints[ destUv + 1]); - const offset = cornerRadius / Math.tan(Math.acos(cosTheta) / 2); + for (let i = 0; 2 * i + 1 < basePoints.length; i++) { + p[i] = {x: centerX + halfW * basePoints[2 * i], y: centerY + halfH * basePoints[2 * i + 1]}; + } - const cp0x = px - offset * basePoints[ sourceUv ]; - const cp0y = py - offset * basePoints[ sourceUv + 1 ]; - const cp1x = px + offset * basePoints[ destUv ]; - const cp1y = py + offset * basePoints[ destUv + 1 ]; + let i, p1, p2, p3, len = p.length; + p1 = p[len - 1]; + // for each point + for (i = 0; i < len; i++) { + p2 = p[(i) % len]; + p3 = p[(i + 1) % len]; + let corner = round.getRoundCorner(p1, p2, p3, cornerRadius); if (i === 0) { - lines[basePoints.length - 2] = cp0x; - lines[basePoints.length - 1] = cp0y; + lines[lines.length - 2] = corner.startX; + lines[lines.length - 1] = corner.startY; } else { - lines[i * 4 - 2] = cp0x; - lines[i * 4 - 1] = cp0y; + lines[i * 4 - 2] = corner.startX; + lines[i * 4 - 1] = corner.startY; } - lines[i * 4] = cp1x; - lines[i * 4 + 1] = cp1y; + lines[i * 4] = corner.stopX; + lines[i * 4 + 1] = corner.stopY; - let orthx = basePoints[ sourceUv + 1 ]; - let orthy = -basePoints[ sourceUv ]; - const cosAlpha = orthx * basePoints[ destUv ] + orthy * basePoints [ destUv + 1 ]; - if (cosAlpha < 0) { - orthx *= -1; - orthy *= -1; - } + intersection = intersectLineCircle(x, y, centerX, centerY, corner.cx, corner.cy, corner.radius); - const cx = cp0x + orthx * cornerRadius; - const cy = cp0y + orthy * cornerRadius; - - intersection = intersectLineCircle(x, y, centerX, centerY, cx, cy, cornerRadius); - - if( intersection.length !== 0 ){ - intersections.push( intersection[0], intersection[1] ); + if (intersection.length !== 0) { + intersections.push(intersection[0], intersection[1]); } + + p1 = p2; + p2 = p3; } for( let i = 0; i < lines.length / 4; i++ ) { diff --git a/src/round.js b/src/round.js new file mode 100644 index 0000000000..811fcb8ab7 --- /dev/null +++ b/src/round.js @@ -0,0 +1,153 @@ +/** + * Explained by Blindman67 at https://stackoverflow.com/a/44856925/11028828 + */ + + +// Declare reused variable to avoid reallocating variables every time the function is called +let x, y, v1 = {}, v2 = {}, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut, radius; +let startX, startY, stopX, stopY; + +// convert 2 points into vector form, polar form, and normalised +const asVec = function (p, pp, v) { + v.x = pp.x - p.x; + v.y = pp.y - p.y; + v.len = Math.sqrt(v.x * v.x + v.y * v.y); + v.nx = v.x / v.len; + v.ny = v.y / v.len; + v.ang = Math.atan2(v.ny, v.nx); +} + +const invertVec = function (v) { + v.x *= -1; + v.y *= -1; + v.nx *= -1; + v.ny *= -1; + v.ang = Math.atan2(v.ny, v.nx); +} + +const calcCornerArc = (previousPoint, currentPoint, nextPoint, radiusMax) => { + //----------------------------------------- + // Part 1 + asVec(currentPoint, previousPoint, v1); + asVec(currentPoint, nextPoint, v2); + sinA = v1.nx * v2.ny - v1.ny * v2.nx; + sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; + angle = angle = Math.asin(Math.max(-1, Math.min(1, sinA))); + //----------------------------------------- + radDirection = 1; + drawDirection = false; + if (sinA90 < 0) { + if (angle < 0) { + angle = Math.PI + angle; + } else { + angle = Math.PI - angle; + radDirection = -1; + drawDirection = true; + } + } else { + if (angle > 0) { + radDirection = -1; + drawDirection = true; + } + } + if (currentPoint.radius !== undefined) { + radius = currentPoint.radius; + } else { + radius = radiusMax; + } + //----------------------------------------- + // Part 2 + halfAngle = angle / 2; + //----------------------------------------- + + //----------------------------------------- + // Part 3 + lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle)); + //----------------------------------------- + + //----------------------------------------- + // Special part A + if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { + lenOut = Math.min(v1.len / 2, v2.len / 2); + cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle)); + } else { + cRadius = radius; + } + //----------------------------------------- + // Part 4 + stopX = currentPoint.x + v2.nx * lenOut; + stopY = currentPoint.y + v2.ny * lenOut; + //----------------------------------------- + // Part 5 + x = stopX - v2.ny * cRadius * radDirection; + y = stopY + v2.nx * cRadius * radDirection; + //----------------------------------------- + // Additional Part : calculate start point E + startX = currentPoint.x + v1.nx * lenOut; + startY = currentPoint.y + v1.ny * lenOut; +} + + +/** + * Draw round corner from a point and its previous and next neighbours in a path + * + * @param ctx :CanvasRenderingContext2D + * @param previousPoint {{x: number, y:number, radius: number?}} + * @param currentPoint {{x: number, y:number, radius: number?}} + * @param nextPoint {{x: number, y:number, radius: number?}} + * @param radiusMax :number + */ +export function drawRoundCorner(ctx, previousPoint, currentPoint, nextPoint, radiusMax) { + calcCornerArc(previousPoint, currentPoint, nextPoint, radiusMax); + ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection); +} + +/** + * Draw corner provided by {@link getRoundCorner} + * + * @param ctx :CanvasRenderingContext2D + * @param roundCorner {{cx:number, cy:number, radius:number, endAngle: number, startAngle: number, counterClockwise: boolean}} + */ +export function drawPreparedRoundCorner(ctx, roundCorner) { + if (roundCorner.radius === 0) ctx.lineTo(roundCorner.cx, roundCorner.cy); + else ctx.arc(roundCorner.cx, roundCorner.cy, roundCorner.radius, roundCorner.startAngle, roundCorner.endAngle, roundCorner.counterClockwise); +} + +/** + * Get round corner from a point and its previous and next neighbours in a path + * + * @param previousPoint {{x: number, y:number, radius: number?}} + * @param currentPoint {{x: number, y:number, radius: number?}} + * @param nextPoint {{x: number, y:number, radius: number?}} + * @param radiusMax :number + * @return {{ + * cx:number, cy:number, radius:number, + * startX:number, startY:number, + * stopX:number, stopY: number, + * endAngle: number, startAngle: number, counterClockwise: boolean + * }} + */ +export function getRoundCorner(previousPoint, currentPoint, nextPoint, radiusMax) { + if (radiusMax === 0 || currentPoint.radius === 0) return { + cx: currentPoint.x, + cy: currentPoint.y, + radius: 0, + startX: currentPoint.x, + startY: currentPoint.y, + stopX: currentPoint.x, + stopY: currentPoint.y, + startAngle: undefined, + endAngle: undefined, + counterClockwise: undefined + } + + calcCornerArc(previousPoint, currentPoint, nextPoint, radiusMax); + return { + cx: x, cy: y, radius: cRadius, + startX, startY, + stopX, stopY, + startAngle: v1.ang + Math.PI / 2 * radDirection, + endAngle: v2.ang - Math.PI / 2 * radDirection, + counterClockwise: drawDirection + }; +} From 4a245ea8c757d995379272d131ad45c7fa4cea78 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 15 Dec 2023 23:22:46 +0000 Subject: [PATCH 05/21] :sparkles: Support rounded edges (round-segments + round-taxi) --- debug/init.js | 36 ++++++++++++-- .../base/coord-ele-math/edge-arrows.js | 10 ++-- .../coord-ele-math/edge-control-points.js | 47 ++++++++++++++++--- .../renderer/canvas/drawing-edges.js | 20 +++++--- src/style/properties.js | 9 +++- 5 files changed, 97 insertions(+), 25 deletions(-) diff --git a/debug/init.js b/debug/init.js index 5904b838ed..891d0093b2 100644 --- a/debug/init.js +++ b/debug/init.js @@ -8,6 +8,7 @@ var cy, defaultSty, options; .selector('node') .style({ + 'background-opacity': 0.4, 'label': 'data(id)' }) .selector('node#a') @@ -19,9 +20,9 @@ var cy, defaultSty, options; }) .selector('node#b') .style({ - 'shape': 'round-triangle', + 'shape': 'round-hexagon', 'width': 40, - 'corner-radius': 4 + 'corner-radius': 10 }) .selector('edge') @@ -73,6 +74,26 @@ var cy, defaultSty, options; .style({ 'curve-style': 'taxi' }) + .selector('#eg') + .style({ + 'curve-style': 'round-taxi', + 'taxi-radius': 2000 + }) + .selector('#eh') + .style({ + 'curve-style': 'round-segments', + 'segment-distances': [ 0 , 50 , 0 , -50, 0 , 0 , 100 ], + 'segment-weights': [ 0.5, 0.6, 0.7, 0.6, 0.5, 0.8, 0.85], + 'segment-radii': [ 50, 100 ], + }) + .selector('#ei') + .style({ + 'curve-style': 'round-taxi' + }) + .selector('#gh') + .style({ + 'curve-style': 'round-taxi' + }) ; options = { @@ -97,7 +118,10 @@ var cy, defaultSty, options; { data: { id: 'c', weight: 20 } }, { data: { id: 'd', weight: 10 } }, { data: { id: 'e', weight: 75 } }, - { data: { id: 'f', weight: 100 } } + { data: { id: 'f', weight: 100 } }, + { data: { id: 'g', weight: 40 } }, + { data: { id: 'h', weight: 16 } }, + { data: { id: 'i', weight: 16 } }, ], edges: [ @@ -117,7 +141,11 @@ var cy, defaultSty, options; { data: { id: 'de4', weight: 7, source: 'd', target: 'e' } }, { data: { id: 'de5', weight: 7, source: 'd', target: 'e' } }, { data: { id: 'bf', weight: 3, source: 'b', target: 'f' } }, - { data: { id: 'ef', weight: 3, source: 'e', target: 'f' } } + { data: { id: 'ef', weight: 3, source: 'e', target: 'f' } }, + { data: { id: 'eg', weight: 3, source: 'e', target: 'g' } }, + { data: { id: 'eh', weight: 3, source: 'e', target: 'h' } }, + { data: { id: 'ei', weight: 3, source: 'e', target: 'i' } }, + { data: { id: 'gh', weight: 3, source: 'g', target: 'h' } }, ] } }; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js index 216d2d70e4..e4ca6aaf24 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js @@ -74,12 +74,8 @@ BRp.calculateArrowAngles = function( edge ){ dispX = ( pts[ i2 ] - pts[ i1 ] ); dispY = ( pts[ i2 + 1] - pts[ i1 + 1] ); } else { - var i2 = pts.length / 2 - 1; - var i1 = i2 - 2; - var i3 = i2 + 2; - - dispX = ( pts[ i2 ] - pts[ i1 ] ); - dispY = ( pts[ i2 + 1] - pts[ i1 + 1] ); + dispX = rs.midVector[1]; + dispY = -rs.midVector[0]; } } else if( isMultibezier || isCompound || isSelf ){ var pts = rs.allpts; @@ -129,7 +125,7 @@ BRp.calculateArrowAngles = function( edge ){ if( pts.length / 2 % 2 === 0 ){ // already ok - } else { + } else if( !rs.isRound ){ var i2 = pts.length / 2 - 1; var i3 = i2 + 2; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js index 5e08923033..aaf9047d2a 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js @@ -2,6 +2,7 @@ import * as math from '../../../../math'; import * as is from '../../../../is'; import * as util from '../../../../util'; import Map from '../../../../map'; +import {getRoundCorner} from "../../../../round"; let BRp = {}; @@ -117,10 +118,14 @@ BRp.findSegmentsPoints = function( edge, pairInfo ){ const rs = edge._private.rscratch; const segmentWs = edge.pstyle( 'segment-weights' ); const segmentDs = edge.pstyle( 'segment-distances' ); + const segmentRs = edge.pstyle( 'segment-radii' ); const segmentsN = Math.min( segmentWs.pfValue.length, segmentDs.pfValue.length ); + const lastRadius = segmentRs.pfValue[ segmentRs.pfValue.length - 1 ]; + rs.edgeType = 'segments'; rs.segpts = []; + rs.radii = []; for( let s = 0; s < segmentsN; s++ ){ let w = segmentWs.pfValue[ s ]; @@ -140,6 +145,8 @@ BRp.findSegmentsPoints = function( edge, pairInfo ){ adjustedMidpt.x + vectorNormInverse.x * d, adjustedMidpt.y + vectorNormInverse.y * d ); + + rs.radii.push( segmentRs.pfValue[ s ] || lastRadius ); } }; @@ -455,6 +462,8 @@ BRp.findTaxiPoints = function( edge, pairInfo ){ x, y2 ]; } + const radius = edge.pstyle( 'taxi-radius' ).value; + rs.radii = new Array( rs.segpts.length / 2 ).fill( radius ) } }; @@ -625,9 +634,33 @@ BRp.storeAllpts = function( edge ){ rs.midY = ( rs.segpts[ i1 + 1] + rs.segpts[ i2 + 1] ) / 2; } else { let i1 = rs.segpts.length / 2 - 1; + if( !rs.isRound ){ + rs.midX = rs.segpts[ i1 ]; + rs.midY = rs.segpts[ i1 + 1 ]; + } else { - rs.midX = rs.segpts[ i1 ]; - rs.midY = rs.segpts[ i1 + 1]; + let radius = rs.radii[ i1 / 2 ]; + let point = { x: rs.segpts[ i1 ], y: rs.segpts[ i1 + 1 ], radius }; + const corner = getRoundCorner( + { x: rs.segpts[ i1 - 2 ] || rs.startX, y: rs.segpts[ i1 - 1 ] || rs.startY }, + point, + { x: rs.segpts[ i1 + 2 ] || rs.endX, y: rs.segpts[ i1 + 3 ] || rs.endY }, + radius + ) + + let v = [ + point.x - corner.cx, + point.y - corner.cy + ]; + + const factor = corner.radius / Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)) + + v = v.map(c => c * factor) + + rs.midX = corner.cx + v[0]; + rs.midY = corner.cy + v[1]; + rs.midVector = v; + } } @@ -697,7 +730,7 @@ BRp.findEdgeControlPoints = function( edges ){ continue; } - let edgeIsUnbundled = curveStyle === 'unbundled-bezier' || curveStyle === 'segments' || curveStyle === 'straight' || curveStyle === 'straight-triangle' || curveStyle === 'taxi'; + let edgeIsUnbundled = curveStyle === 'unbundled-bezier' || curveStyle.endsWith('segments') || curveStyle === 'straight' || curveStyle === 'straight-triangle' || curveStyle.endsWith('taxi'); let edgeIsBezier = curveStyle === 'unbundled-bezier' || curveStyle === 'bezier'; let src = _p.source; let tgt = _p.target; @@ -785,7 +818,7 @@ BRp.findEdgeControlPoints = function( edges ){ const edge = pairInfo.eles[i]; const rs = edge[0]._private.rscratch; const curveStyle = edge.pstyle( 'curve-style' ).value; - const edgeIsUnbundled = curveStyle === 'unbundled-bezier' || curveStyle === 'segments' || curveStyle === 'taxi'; + const edgeIsUnbundled = curveStyle === 'unbundled-bezier' || curveStyle.endsWith('segments') || curveStyle.endsWith('taxi'); // whether the normalised pair order is the reverse of the edge's src-tgt order const edgeIsSwapped = !src.same(edge.source()); @@ -892,6 +925,8 @@ BRp.findEdgeControlPoints = function( edges ){ rs.srcIntn = passedPairInfo.srcIntn; rs.tgtIntn = passedPairInfo.tgtIntn; + rs.isRound = curveStyle.startsWith('round'); + if( hasCompounds && ( src.isParent() || src.isChild() || tgt.isParent() || tgt.isChild() ) && @@ -902,10 +937,10 @@ BRp.findEdgeControlPoints = function( edges ){ } else if( src === tgt ){ this.findLoopPoints(edge, passedPairInfo, i, edgeIsUnbundled); - } else if( curveStyle === 'segments' ){ + } else if( curveStyle.endsWith( 'segments' )){ this.findSegmentsPoints(edge, passedPairInfo); - } else if( curveStyle === 'taxi' ){ + } else if( curveStyle.endsWith( 'taxi' )){ this.findTaxiPoints(edge, passedPairInfo); } else if( diff --git a/src/extensions/renderer/canvas/drawing-edges.js b/src/extensions/renderer/canvas/drawing-edges.js index d25270dda8..37eebb3fc6 100644 --- a/src/extensions/renderer/canvas/drawing-edges.js +++ b/src/extensions/renderer/canvas/drawing-edges.js @@ -1,7 +1,7 @@ /* global Path2D */ import * as util from '../../../util'; -import {drawRoundCorner} from "./drawing-shapes"; +import {drawRoundCorner} from "../../../round"; let CRp = {}; @@ -213,13 +213,19 @@ CRp.drawEdgePath = function( edge, context, pts, type ){ } break; case 'segments': - for( let i = 2; i + 3 < pts.length; i += 2 ){ - drawRoundCorner(context, - {x: pts[i - 2], y: pts[i - 1]}, - {x: pts[i], y: pts[i + 1]}, - {x: pts[i + 2], y: pts[i + 3]}, 20000) + if (rs.isRound) { + for( let i = 2; i + 3 < pts.length; i += 2 ){ + drawRoundCorner(context, + {x: pts[i - 2], y: pts[i - 1]}, + {x: pts[i], y: pts[i + 1], radius: rs.radii[ (i / 2) - 1]}, + {x: pts[i + 2], y: pts[i + 3]}, Infinity) + } + context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] ); + } else { + for( let i = 2; i + 1 < pts.length; i += 2 ) { + context.lineTo( pts[ i ], pts[ i + 1] ); + } } - context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] ); break; } } diff --git a/src/style/properties.js b/src/style/properties.js index 3b3e0d30da..4ed9684b7f 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -58,7 +58,7 @@ const styfn = {}; lineStyle: { enums: [ 'solid', 'dotted', 'dashed' ] }, lineCap: { enums: [ 'butt', 'round', 'square' ] }, borderStyle: { enums: [ 'solid', 'dotted', 'dashed', 'double' ] }, - curveStyle: { enums: [ 'bezier', 'unbundled-bezier', 'haystack', 'segments', 'straight', 'straight-triangle', 'taxi' ] }, + curveStyle: { enums: [ 'bezier', 'unbundled-bezier', 'haystack', 'segments', 'straight', 'straight-triangle', 'taxi', 'round-segments', 'round-taxi' ] }, fontFamily: { regex: '^([\\w- \\"]+(?:\\s*,\\s*[\\w- \\"]+)*)$' }, fontStyle: { enums: [ 'italic', 'normal', 'oblique' ] }, fontWeight: { enums: [ 'normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '800', '900', 100, 200, 300, 400, 500, 600, 700, 800, 900 ] }, @@ -358,9 +358,11 @@ const styfn = {}; { name: 'control-point-weights', type: t.numbers, triggersBounds: diff.any }, { name: 'segment-distances', type: t.bidirectionalSizes, triggersBounds: diff.any }, { name: 'segment-weights', type: t.numbers, triggersBounds: diff.any }, + { name: 'segment-radii', type: t.numbers, triggersBounds: diff.any }, { name: 'taxi-turn', type: t.bidirectionalSizeMaybePercent, triggersBounds: diff.any }, { name: 'taxi-turn-min-distance', type: t.size, triggersBounds: diff.any }, { name: 'taxi-direction', type: t.axisDirection, triggersBounds: diff.any }, + { name: 'taxi-radius', type: t.number, triggersBounds: diff.any }, { name: 'edge-distances', type: t.edgeDistances, triggersBounds: diff.any }, { name: 'arrow-scale', type: t.positiveNumber, triggersBounds: diff.any }, { name: 'loop-direction', type: t.angle, triggersBounds: diff.any }, @@ -492,6 +494,9 @@ const styfn = {}; { name: 'content', pointsTo: 'label' }, { name: 'control-point-distance', pointsTo: 'control-point-distances' }, { name: 'control-point-weight', pointsTo: 'control-point-weights' }, + { name: 'segment-distance', pointsTo: 'segment-distances' }, + { name: 'segment-weight', pointsTo: 'segment-weights' }, + { name: 'segment-radius', pointsTo: 'segment-radii' }, { name: 'edge-text-rotation', pointsTo: 'text-rotation' }, { name: 'padding-left', pointsTo: 'padding' }, { name: 'padding-right', pointsTo: 'padding' }, @@ -700,7 +705,9 @@ styfn.getDefaultProperties = function(){ 'control-point-weights': 0.5, 'segment-weights': 0.5, 'segment-distances': 20, + 'segment-radii': 15, 'taxi-turn': '50%', + 'taxi-radius': 15, 'taxi-turn-min-distance': 10, 'taxi-direction': 'auto', 'edge-distances': 'intersection', From 8bf1725f5a3afe76281fb0dabc6d481633b4e1a8 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Sat, 16 Dec 2023 00:20:26 +0000 Subject: [PATCH 06/21] :memo::boom: Start documentation + WARNING noticed bug within demos/edge-types, need fixing --- debug/init.js | 5 +- documentation/demos/edge-types/cy-style.json | 16 +++++ documentation/demos/edge-types/data.json | 72 ++++++++++++++++---- documentation/md/style.md | 2 +- 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/debug/init.js b/debug/init.js index 891d0093b2..31337d9977 100644 --- a/debug/init.js +++ b/debug/init.js @@ -77,7 +77,10 @@ var cy, defaultSty, options; .selector('#eg') .style({ 'curve-style': 'round-taxi', - 'taxi-radius': 2000 + "taxi-direction": "downward", + // "taxi-turn": 100, + "taxi-turn-min-distance": 50, + "taxi-radius": 50 }) .selector('#eh') .style({ diff --git a/documentation/demos/edge-types/cy-style.json b/documentation/demos/edge-types/cy-style.json index ee30fb03bb..3470a80f3f 100644 --- a/documentation/demos/edge-types/cy-style.json +++ b/documentation/demos/edge-types/cy-style.json @@ -53,6 +53,13 @@ "segment-weights": [0.250 , 0.75] } }, { + "selector": "edge.round-segments", + "style": { + "curve-style": "round-segments", + "segment-distances": [ 40, -40 ], + "segment-weights": [0.250 , 0.75] + } +}, { "selector": "edge.taxi", "style": { "curve-style": "taxi", @@ -60,6 +67,15 @@ "taxi-turn": 20, "taxi-turn-min-distance": 5 } +}, { + "selector": "edge.round-taxi", + "style": { + "curve-style": "round-taxi", + "taxi-direction": "downward", + "taxi-turn": 20, + "taxi-turn-min-distance": 5, + "taxi-radius": 10 + } }, { "selector": "edge.straight-triangle", "style": { diff --git a/documentation/demos/edge-types/data.json b/documentation/demos/edge-types/data.json index 759ed8666f..1043c16726 100644 --- a/documentation/demos/edge-types/data.json +++ b/documentation/demos/edge-types/data.json @@ -122,56 +122,100 @@ "target": "n10" }, "classes": "segments" +}, + { + "data": { + "id": "n11" + } + }, { + "data": { + "id": "n12", + "type": "round-segments" + } }, { "data": { - "id": "n11" + "source": "n11", + "target": "n12" + }, + "classes": "round-segments" +} +, { + "data": { + "id": "n13" } }, { "data": { - "id": "n12" + "id": "n14" } }, { "data": { - "id": "n13", + "id": "n15", "type": "taxi" } }, { "data": { - "source": "n13", - "target": "n11" + "source": "n15", + "target": "n13" }, "classes": "taxi" }, { "data": { - "source": "n13", - "target": "n12" + "source": "n15", + "target": "n14" }, "classes": "taxi" +}, + { + "data": { + "id": "n16" + } }, { "data": { - "id": "n16", + "id": "n17" + } +}, { + "data": { + "id": "n18", + "type": "round-taxi" + } +}, { + "data": { + "source": "n18", + "target": "n16" + }, + "classes": "round-taxi" +}, { + "data": { + "source": "n18", + "target": "n17" + }, + "classes": "round-taxi" +}, + { + "data": { + "id": "n19", "type": "loop", "flipLabel": true } }, { "data": { - "source": "n16", - "target": "n16" + "source": "n19", + "target": "n19" }, "classes": "loop" }, { "data": { - "id": "n17", + "id": "n20", "type": "straight-triangle" } }, { "data": { - "id": "n18" + "id": "n21" } }, { "data": { - "source": "n17", - "target": "n18" + "source": "n20", + "target": "n21" }, "classes": "straight-triangle" }] diff --git a/documentation/md/style.md b/documentation/md/style.md index 24a2653af7..45d70ea8b9 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -357,7 +357,7 @@ You may find it useful to reserve a number to a particular colour for all nodes These properties affect the styling of an edge's line: * **`width`** : The width of an edge's line. - * **`curve-style`** : The curving method used to separate two or more edges between two nodes ([demo](demos/edge-types)); may be [`haystack`](#style/haystack-edges) (default, very fast, bundled straight edges for which loops and compounds are unsupported), [`straight`](#style/straight-edges) (straight edges with all arrows supported), [`straight-triangle`](#style/straight-triangle-edges) (straight triangle edges), [`bezier`](#style/bezier-edges) (bundled curved edges), [`unbundled-bezier`](#style/unbundled-bezier-edges) (curved edges for use with manual control points), [`segments`](#style/segments-edges) (a series of straight lines), [`taxi`](#style/taxi-edges) (right-angled lines, hierarchically bundled). Note that `haystack` edges work best with `ellipse`, `rectangle`, or similar nodes. Smaller node shapes, like `triangle`, will not be as aesthetically pleasing. Also note that edge endpoint arrows are unsupported for `haystack` edges. + * **`curve-style`** : The curving method used to separate two or more edges between two nodes ([demo](demos/edge-types)); may be [`haystack`](#style/haystack-edges) (default, very fast, bundled straight edges for which loops and compounds are unsupported), [`straight`](#style/straight-edges) (straight edges with all arrows supported), [`straight-triangle`](#style/straight-triangle-edges) (straight triangle edges), [`bezier`](#style/bezier-edges) (bundled curved edges), [`unbundled-bezier`](#style/unbundled-bezier-edges) (curved edges for use with manual control points), [`segments`](#style/segments-edges) (a series of straight lines), [`round-segments`](#style/round-segments-edges) (a series of straight lines with rounded corners), [`taxi`](#style/taxi-edges) (right-angled lines, hierarchically bundled), [`round-taxi`](#style/round-taxi-edges) (right-angled lines, hierarchically bundled, with rounded corners). Note that `haystack` edges work best with `ellipse`, `rectangle`, or similar nodes. Smaller node shapes, like `triangle`, will not be as aesthetically pleasing. Also note that edge endpoint arrows are unsupported for `haystack` edges. * **`line-color`** : The colour of the edge's line. * **`line-style`** : The style of the edge's line; may be `solid`, `dotted`, or `dashed`. * **`line-cap`** : The cap style of the edge's line; may be `butt` (default), `round`, or `square`. The cap may or may not be visible, depending on the shape of the node and the relative size of the node and edge. Caps other than `butt` extend beyond the specified endpoint of the edge. From 6f67e969603703780b683b499d1fe18fb78589e4 Mon Sep 17 00:00:00 2001 From: cqgong Date: Thu, 21 Dec 2023 16:01:03 +0000 Subject: [PATCH 07/21] :lipstick: update edge types demo --- documentation/demos/edge-types/code.js | 38 +++-- documentation/demos/edge-types/data.json | 135 +++++++++--------- .../base/coord-ele-math/edge-arrows.js | 10 +- 3 files changed, 101 insertions(+), 82 deletions(-) diff --git a/documentation/demos/edge-types/code.js b/documentation/demos/edge-types/code.js index 3fcbfbf090..663afbc6d1 100644 --- a/documentation/demos/edge-types/code.js +++ b/documentation/demos/edge-types/code.js @@ -17,18 +17,34 @@ }); cy.ready(function(){ // make taxi nodes better organised - var n13 = cy.$('#n13'); - var n11 = cy.$('#n11'); - var n12 = cy.$('#n12'); - var p11 = n11.position(); - var p12 = n12.position(); - var d = (p12.x - p11.x)/4; - - n13.position({ - x: (p11.x + p12.x)/2, - y: p11.y - d + var n19 = cy.$('#n19'); + var n17 = cy.$('#n17'); + var n18 = cy.$('#n18'); + var p17 = n17.position(); + var p18 = n18.position(); + var d = (p18.x - p17.x)/4; + + n19.position({ + x: (p17.x + p18.x)/2, + y: p17.y - d }); - n11.add(n12).position({ y: p11.y + d }); + n17.add(n18).position({ y: p17.y + d }); + + //// make round-taxi nodes better organised + var n23 = cy.$('#n23'); + var n21 = cy.$('#n21'); + var n22 = cy.$('#n22'); + var p21 = n21.position(); + var p22 = n22.position(); + var dr = (p22.x - p21.x)/4; + + n23.position({ + x: (p21.x + p22.x)/2, + y: p21.y + }); + + n21.add(n22).position({ y: p21.y + 2 * dr }); + }); })(); \ No newline at end of file diff --git a/documentation/demos/edge-types/data.json b/documentation/demos/edge-types/data.json index 1043c16726..6622a41594 100644 --- a/documentation/demos/edge-types/data.json +++ b/documentation/demos/edge-types/data.json @@ -58,164 +58,163 @@ "classes": "multi-unbundled-bezier" }, { "data": { - "id": "n14" + "id": "n07" } }, { "data": { - "id": "n15", + "id": "n08", "type": "straight", "flipLabel": true } }, { "data": { - "source": "n14", - "target": "n15" + "source": "n07", + "target": "n08" }, "classes": "straight" }, { "data": { - "id": "n07", + "id": "n09", "type": "haystack" } }, { "data": { - "id": "n08" + "id": "n10" } }, { "data": { "id": "e06", - "source": "n08", - "target": "n07" + "source": "n10", + "target": "n09" }, "classes": "haystack" }, { "data": { - "source": "n08", - "target": "n07" + "source": "n10", + "target": "n09" }, "classes": "haystack" }, { "data": { - "source": "n08", - "target": "n07" + "source": "n10", + "target": "n09" }, "classes": "haystack" }, { "data": { - "source": "n08", - "target": "n07" + "source": "n10", + "target": "n09" }, "classes": "haystack" +}, { + "data": { + "id": "n11" + } +}, { + "data": { + "id": "n12", + "type": "straight-triangle", + "flipLabel": true + } +}, { + "data": { + "source": "n11", + "target": "n12" + }, + "classes": "straight-triangle" }, { "data": { - "id": "n09" + "id": "n13" } }, { "data": { - "id": "n10", + "id": "n14", "type": "segments", "flipLabel": true } }, { "data": { - "source": "n09", - "target": "n10" + "source": "n13", + "target": "n14" }, "classes": "segments" }, { "data": { - "id": "n11" + "id": "n15" } }, { "data": { - "id": "n12", - "type": "round-segments" + "id": "n16", + "type": "round-segments", + "flipLabel": true } }, { "data": { - "source": "n11", - "target": "n12" + "source": "n15", + "target": "n16" }, "classes": "round-segments" -} -, { +},{ "data": { - "id": "n13" + "id": "n17" } }, { "data": { - "id": "n14" + "id": "n18" } }, { "data": { - "id": "n15", + "id": "n19", "type": "taxi" } }, { "data": { - "source": "n15", - "target": "n13" + "source": "n19", + "target": "n17" }, "classes": "taxi" }, { "data": { - "source": "n15", - "target": "n14" + "source": "n19", + "target": "n18" }, "classes": "taxi" -}, - { +},{ "data": { - "id": "n16" - } -}, { - "data": { - "id": "n17" - } -}, { - "data": { - "id": "n18", - "type": "round-taxi" - } -}, { - "data": { - "source": "n18", - "target": "n16" - }, - "classes": "round-taxi" -}, { - "data": { - "source": "n18", - "target": "n17" - }, - "classes": "round-taxi" -}, - { - "data": { - "id": "n19", + "id": "n20", "type": "loop", "flipLabel": true } }, { "data": { - "source": "n19", - "target": "n19" + "source": "n20", + "target": "n20" }, "classes": "loop" }, { "data": { - "id": "n20", - "type": "straight-triangle" + "id": "n21" } }, { "data": { - "id": "n21" + "id": "n22" } }, { "data": { - "source": "n20", + "id": "n23", + "type": "round-taxi" + } +}, { + "data": { + "source": "n23", "target": "n21" }, - "classes": "straight-triangle" + "classes": "round-taxi" +}, { + "data": { + "source": "n23", + "target": "n22" + }, + "classes": "round-taxi" }] diff --git a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js index e4ca6aaf24..1d878e0059 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js @@ -73,10 +73,14 @@ BRp.calculateArrowAngles = function( edge ){ dispX = ( pts[ i2 ] - pts[ i1 ] ); dispY = ( pts[ i2 + 1] - pts[ i1 + 1] ); - } else { - dispX = rs.midVector[1]; - dispY = -rs.midVector[0]; } + + //todo: the code below cause undefined property "1" error + // + // else { + // // dispX = rs.midVector[1]; + // // dispY = -rs.midVector[0]; + // } } else if( isMultibezier || isCompound || isSelf ){ var pts = rs.allpts; var cpts = rs.ctrlpts; From e4ecb4497e7d6ec7d066ba0bf512686ccea85620 Mon Sep 17 00:00:00 2001 From: cqgong Date: Thu, 21 Dec 2023 16:37:13 +0000 Subject: [PATCH 08/21] :lipstick: grid layout up to 6 columns in edge types demo --- documentation/demos/edge-types/code.js | 20 ++++----- documentation/demos/edge-types/data.json | 57 +++++++++++++----------- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/documentation/demos/edge-types/code.js b/documentation/demos/edge-types/code.js index 663afbc6d1..739baf3c56 100644 --- a/documentation/demos/edge-types/code.js +++ b/documentation/demos/edge-types/code.js @@ -8,7 +8,7 @@ layout: { name: 'grid', - columns: 4 + columns: 6 }, style: fetch('cy-style.json').then(toJson), @@ -31,20 +31,20 @@ n17.add(n18).position({ y: p17.y + d }); - //// make round-taxi nodes better organised - var n23 = cy.$('#n23'); - var n21 = cy.$('#n21'); + // make round-taxi nodes better organised var n22 = cy.$('#n22'); + var n20 = cy.$('#n20'); + var n21 = cy.$('#n21'); + var p20 = n20.position(); var p21 = n21.position(); - var p22 = n22.position(); - var dr = (p22.x - p21.x)/4; + var dr = (p21.x - p20.x)/4; - n23.position({ - x: (p21.x + p22.x)/2, - y: p21.y + n22.position({ + x: (p20.x + p21.x)/2, + y: p20.y -d }); - n21.add(n22).position({ y: p21.y + 2 * dr }); + n20.add(n21).position({ y: p20.y + dr }); }); })(); \ No newline at end of file diff --git a/documentation/demos/edge-types/data.json b/documentation/demos/edge-types/data.json index 6622a41594..e2b62b19aa 100644 --- a/documentation/demos/edge-types/data.json +++ b/documentation/demos/edge-types/data.json @@ -1,11 +1,12 @@ [{ "data": { - "id": "n01", - "type": "bezier" + "id": "n01" } }, { "data": { - "id": "n02" + "id": "n02", + "type": "bezier", + "flipLabel": true } }, { "data": { @@ -43,12 +44,13 @@ "classes": "unbundled-bezier" }, { "data": { - "id": "n05", - "type": "unbundled-bezier(multiple)" + "id": "n05" } }, { "data": { - "id": "n06" + "id": "n06", + "type": "unbundled-bezier(multiple)", + "flipLabel": true } }, { "data": { @@ -74,12 +76,13 @@ "classes": "straight" }, { "data": { - "id": "n09", - "type": "haystack" + "id": "n09" } }, { "data": { - "id": "n10" + "id": "n10", + "type": "haystack", + "flipLabel": true } }, { "data": { @@ -180,41 +183,41 @@ "target": "n18" }, "classes": "taxi" -},{ - "data": { - "id": "n20", - "type": "loop", - "flipLabel": true - } }, { "data": { - "source": "n20", - "target": "n20" - }, - "classes": "loop" + "id": "n20" + } }, { "data": { "id": "n21" } }, { "data": { - "id": "n22" + "id": "n22", + "type": "round-taxi" } }, { "data": { - "id": "n23", - "type": "round-taxi" - } + "source": "n22", + "target": "n20" + }, + "classes": "round-taxi" }, { "data": { - "source": "n23", + "source": "n22", "target": "n21" }, "classes": "round-taxi" +},{ + "data": { + "id": "n23", + "type": "loop", + "flipLabel": true + } }, { "data": { "source": "n23", - "target": "n22" + "target": "n23" }, - "classes": "round-taxi" -}] + "classes": "loop"} +] From 15c32ec39afdfba20db8072641e61c36a1597f99 Mon Sep 17 00:00:00 2001 From: cqgong Date: Fri, 22 Dec 2023 10:30:59 +0000 Subject: [PATCH 09/21] :lipstick: round edge types documentation --- documentation/md/style.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/documentation/md/style.md b/documentation/md/style.md index 45d70ea8b9..c9af37c80a 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -451,6 +451,11 @@ A segment edge is made of a series of one or more straight lines, using a co-ord * A manual endpoint may be specified with a position, e.g. `source-endpoint: 20 10`. * A manual endpoint may be alternatively specified with an angle, e.g. `target-endpoint: 90deg`. +## Round segments edges + +For rounded edges made of several straight lines (`curve-style: round-segments`, [demo](demos/edge-types)): + +A new segment edge type is introduced to facilitate the seamless creation of rounded segment edges, utilizing the same property as * **`segment-edges`**. ## Straight edges @@ -497,6 +502,11 @@ When a taxi edge would be impossible to draw along the regular turning plan --- * This property makes the taxi edge be re-routed when the turns would be otherwise too close to the source or target. As such, it also helps to avoid turns overlapping edge endpoint arrows. * **`edge-distances`** : With value `intersection` (default), the distances (`taxi-turn` and `taxi-turn-min-distance`) are considered from the outside of the source's bounds to the outside of the target's bounds. With value `node-position`, the distances are considered from the source position to the target position. The `node-position` option makes calculating edge points easier --- but it should be used carefully because you can create invalid points that `intersection` would have automatically corrected. +## Round taxi edges + +Apply the round style to Taxi edges (`curve-style: round-taxi`, [demo](demos/edge-types)): + +Similar to rounded segment edges, this round text edge type allows for smooth curvature to achieve a polished appearance. ## Edge arrow From 8bfdee27a4f783bb8c231c537cf1f5e0385703fb Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Mon, 8 Jan 2024 14:36:20 +0000 Subject: [PATCH 10/21] :memo: Avoid using midVector cache when not available (not round) --- .../renderer/base/coord-ele-math/edge-arrows.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js index 1d878e0059..6413e99422 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-arrows.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-arrows.js @@ -73,14 +73,16 @@ BRp.calculateArrowAngles = function( edge ){ dispX = ( pts[ i2 ] - pts[ i1 ] ); dispY = ( pts[ i2 + 1] - pts[ i1 + 1] ); - } + } else if( rs.isRound ){ + dispX = rs.midVector[1]; + dispY = -rs.midVector[0]; + } else { + var i2 = pts.length / 2 - 1; + var i1 = i2 - 2; - //todo: the code below cause undefined property "1" error - // - // else { - // // dispX = rs.midVector[1]; - // // dispY = -rs.midVector[0]; - // } + dispX = ( pts[ i2 ] - pts[ i1 ] ); + dispY = ( pts[ i2 + 1] - pts[ i1 + 1] ); + } } else if( isMultibezier || isCompound || isSelf ){ var pts = rs.allpts; var cpts = rs.ctrlpts; From 53ccf2ff7686d149b39b4dd4a102b4275b5ebcbf Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Mon, 8 Jan 2024 14:58:44 +0000 Subject: [PATCH 11/21] :memo: Update documentation --- documentation/md/style.md | 40 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/documentation/md/style.md b/documentation/md/style.md index c9af37c80a..084258edcd 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -455,8 +455,18 @@ A segment edge is made of a series of one or more straight lines, using a co-ord For rounded edges made of several straight lines (`curve-style: round-segments`, [demo](demos/edge-types)): -A new segment edge type is introduced to facilitate the seamless creation of rounded segment edges, utilizing the same property as * **`segment-edges`**. +A round segment edge type is made of a series of one or more straight lines, joined together by a round corner, using a co-ordinate system relative to the source and target nodes. This maintains the overall line pattern regardless of the orientation of the positions of the source and target nodes. +* **`segment-distances`** : A series of values that specify for each segment point the distance perpendicular to a line formed from source to target, e.g. `-20 20 -20`. +* **`segment-weights`** : A series of values that weights segment points along a line from source to target, e.g. `0.25 0.5 0.75`. A value usually ranges on [0, 1], with 0 towards the source node and 1 towards the target node --- but larger or smaller values can also be used. +* **`segment-radii`** : A series of values that provide the radii of the different points positioned by `segment-distances` and `segment-weights`, e.g. `15 0 5`. If less radii are provided tha points have been defined, the last provided radius will be used for all the missing radius. If a single radius is provided, it will therefore be applied to all the segment's points. +* **`edge-distances`** : + * With value `intersection` (default), the line from source to target for `segment-weights` is from the outside of the source node's shape to the outside of the target node's shape. + * With value `node-position`, the line is from the source position to the target position. + * The `node-position` option makes calculating edge points easier --- but it should be used carefully because you can create invalid points that `intersection` would have automatically corrected. + * With value `endpoints`, the line is from the manually-specified source endpoint (via `source-endpoint`) to the manually-specified target endpoint (via `target-endpoint`). + * A manual endpoint may be specified with a position, e.g. `source-endpoint: 20 10`. + * A manual endpoint may be alternatively specified with an angle, e.g. `target-endpoint: 90deg`. ## Straight edges For straight line edges (`curve-style: straight`, [demo](demos/edge-types)): @@ -506,7 +516,33 @@ When a taxi edge would be impossible to draw along the regular turning plan --- Apply the round style to Taxi edges (`curve-style: round-taxi`, [demo](demos/edge-types)): -Similar to rounded segment edges, this round text edge type allows for smooth curvature to achieve a polished appearance. +For hierarchical, bundled edges (`curve-style: taxi`, [demo](demos/edge-types)): + +A round taxi edge (`curve-style: round-taxi`) is drawn as a series of right-angled lines, with rounded corners (i.e. in [taxicab geometry](https://en.wikipedia.org/wiki/Taxicab_geometry)). The edge has a primary direction along either the x-axis or y-axis, which can be used to bundle edges in a hierarchy. That is, taxi edges are appropriate for trees and DAGs that are laid out in a hierarchical manner. + +A round taxi edge has at most two visible turns: Starting from the source node, the edge goes in the primary direction for the specified distance. The edge then turns, going towards the target along the secondary axis. The first turn can be specified in order to bundle the edges of outgoing nodes. The second turn is implicit, based on the first turn, going the remaining distance along the main axis. + +When a taxi edge would be impossible to draw along the regular turning plan --- i.e. one or more turns is too close the source or target --- it is re-routed. The re-routing is carried out on a best-effort basis: Re-routing prioritises the specified direction for bundling over the specified turn distance. A `downward` edge, for example, will avoid going in the upward direction where possible. In practice, re-routing should not take place for graphs that are well laid out. + + Only `outside-to-node` endpoints are supported for a taxi edge, i.e. `source-endpoint: outside-to-node` and `target-endpoint: outside-to-node`. + +* **`taxi-direction`** : The main direction of the edge, the direction starting out from the source node; may be one of: + * `auto` : Automatically use `vertical` or `horizontal`, based on whether the vertical or horizontal distance is largest. + * `vertical` : Automatically use `downward` or `upward`, based on the vertical direction from source to target. + * `downward` : Bundle outgoers downwards. + * `upward` : Bundle outgoers upwards. + * `horizontal` : Automatically use `righward` or `leftward`, based on the horizontal direction from source to target. + * `rightward` : Bundle outgoers righwards. + * `leftward` : Bundle outgoers leftwards. +* **`taxi-radius`** : The radius of the rounded corners of the edge. +* **`taxi-turn`** : The distance along the primary axis where the first turn is applied. + * This value may be an absolute distance (e.g. `20px`) or it may be a relative distance between the source and target (e.g. `50%`). + * A negative value may be specified to indicate a distance in the oppostite, target to source direction (e.g. `-20px`). + * Note that bundling may not work with an explicit direction (`upward`, `downward`, `leftward`, or `rightward`) in tandem with a turn distance specified in percent units. +* **`taxi-turn-min-distance`** : The minimum distance along the primary axis that is maintained between the nodes and the turns. + * This value only takes on absolute values (e.g. `5px`). + * This property makes the taxi edge be re-routed when the turns would be otherwise too close to the source or target. As such, it also helps to avoid turns overlapping edge endpoint arrows. +* **`edge-distances`** : With value `intersection` (default), the distances (`taxi-turn` and `taxi-turn-min-distance`) are considered from the outside of the source's bounds to the outside of the target's bounds. With value `node-position`, the distances are considered from the source position to the target position. The `node-position` option makes calculating edge points easier --- but it should be used carefully because you can create invalid points that `intersection` would have automatically corrected. ## Edge arrow From 6222b6fc66f1088a94c104305da4ce869644498d Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Mon, 8 Jan 2024 16:17:44 +0000 Subject: [PATCH 12/21] :memo: Add corner radius documentation --- documentation/demos/compound-nodes/code.js | 11 +++++++++++ documentation/md/style.md | 3 +++ src/style/properties.js | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/documentation/demos/compound-nodes/code.js b/documentation/demos/compound-nodes/code.js index fa10a454ae..9b436fac73 100644 --- a/documentation/demos/compound-nodes/code.js +++ b/documentation/demos/compound-nodes/code.js @@ -7,6 +7,7 @@ var cy = window.cy = cytoscape({ { selector: 'node', css: { + 'shape': 'rectangle', 'content': 'data(id)', 'text-valign': 'center', 'text-halign': 'center' @@ -17,6 +18,16 @@ var cy = window.cy = cytoscape({ css: { 'text-valign': 'top', 'text-halign': 'center', + 'shape': 'round-rectangle', + 'corner-radius': "10", + 'padding': 10 + } + }, + { + selector: 'node#e', + css: { + 'corner-radius': "10", + 'padding': 0 } }, { diff --git a/documentation/md/style.md b/documentation/md/style.md index 084258edcd..6ac9155cf7 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -193,6 +193,9 @@ Shape: * `vee` * `polygon` (custom polygon specified via `shape-polygon-points`). * **`shape-polygon-points`** : An array (or a space-separated string) of numbers ranging on [-1, 1], representing alternating x and y values (i.e. `x1 y1 x2 y2, x3 y3 ...`). This represents the points in the polygon for the node's shape. The bounding box of the node is given by (-1, -1), (1, -1), (1, 1), (-1, 1). The node's position is the origin (0, 0). + * **`corner-radius`** : The corner radius for `round-` shapes, in px or em. + * **WARNING** If you are using corner radius for a parent node (see [compound nodes](#notation/compound-nodes)), you can have children nodes going outside their parent, e.g. node **E** in [compound demo](demos/compound-nodes). + * In order to fix this issue, you can play with the `padding` of the parent node. Having the same value for `padding` and `corner-radius` is always safe, e.g. node **B** in [compound demo](demos/compound-nodes). Background: diff --git a/src/style/properties.js b/src/style/properties.js index 4ed9684b7f..394b5174ab 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -73,7 +73,7 @@ const styfn = {}; 'tag', 'round-tag', 'star', 'diamond', 'round-diamond', 'vee', 'rhomboid', 'right-rhomboid', 'polygon', ] }, overlayShape: { enums: [ 'roundrectangle', 'round-rectangle', 'ellipse' ] }, - cornerRadius: { number: true, min: 0, units: '%|px|em', implicitUnits: 'px', enums: ['auto'] }, + cornerRadius: { number: true, min: 0, units: 'px|em', implicitUnits: 'px', enums: ['auto'] }, compoundIncludeLabels: { enums: [ 'include', 'exclude' ] }, arrowShape: { enums: [ 'tee', 'triangle', 'triangle-tee', 'circle-triangle', 'triangle-cross', 'triangle-backcurve', 'vee', 'square', 'circle', 'diamond', 'chevron', 'none' ] }, arrowFill: { enums: [ 'filled', 'hollow' ] }, From acff4291f03feb69714354931dda7a29de4a4138 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Thu, 11 Jan 2024 11:36:02 +0000 Subject: [PATCH 13/21] :sparkles: update corner radius for outline --- .../renderer/canvas/drawing-nodes.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index f0374fab47..aa3879486d 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -123,7 +123,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } return { - path, + path, cacheHit }; }; @@ -166,7 +166,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } }; - let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => { + let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => { let prevBging = _p.backgrounding; let totalCompleted = 0; @@ -306,12 +306,12 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } let shape = r.getNodeShape( node ); - + let scaleX = (nodeWidth + borderWidth + (outlineWidth + outlineOffset)) / nodeWidth; let scaleY = (nodeHeight + borderWidth + (outlineWidth + outlineOffset)) / nodeHeight; let sWidth = nodeWidth * scaleX; let sHeight = nodeHeight * scaleY; - + let points = r.nodeShapes[ shape ].points; let path; @@ -325,7 +325,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s if (shape === "ellipse") { r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight); } else if ([ - 'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon', + 'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon', 'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag' ].includes(shape)) { let sMult = 0; @@ -354,13 +354,13 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s scaleY = (nodeHeight + sMult)/nodeHeight; } - r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, cornerRadius); + r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, cornerRadius + outlineOffset); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { - r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius); + r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + outlineOffset); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) { - r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius); + r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + outlineOffset); } else if (shape === "barrel") { r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) { @@ -372,7 +372,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s points = joinLines(expandPolygon(points, -pad)); r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); } - + if( usePaths ){ context.stroke( path ); } else { @@ -458,7 +458,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s drawBorder(); drawPie( darkness !== 0 || borderWidth !== 0 ); drawImages(eleOpacity, false); - + darken(); if( usePaths ){ From 9377211ddfde1a24f95060c0e0e1afcb3a879c1d Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 12 Jan 2024 10:38:29 +0000 Subject: [PATCH 14/21] :zap: Optimise round calculation by using previous vectors when possible --- src/round.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/round.js b/src/round.js index 811fcb8ab7..60ba344736 100644 --- a/src/round.js +++ b/src/round.js @@ -6,6 +6,7 @@ // Declare reused variable to avoid reallocating variables every time the function is called let x, y, v1 = {}, v2 = {}, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut, radius; let startX, startY, stopX, stopY; +let lastPoint; // convert 2 points into vector form, polar form, and normalised const asVec = function (p, pp, v) { @@ -17,18 +18,18 @@ const asVec = function (p, pp, v) { v.ang = Math.atan2(v.ny, v.nx); } -const invertVec = function (v) { - v.x *= -1; - v.y *= -1; - v.nx *= -1; - v.ny *= -1; - v.ang = Math.atan2(v.ny, v.nx); -} +const invertVec = function (originalV, invertedV) { + invertedV.x = originalV.x * -1; + invertedV.y = originalV.y * -1; + invertedV.nx = originalV.nx * -1; + invertedV.ny = originalV.ny * -1; + invertedV.ang = Math.atan2(invertedV.ny, invertedV.nx); +}; const calcCornerArc = (previousPoint, currentPoint, nextPoint, radiusMax) => { //----------------------------------------- // Part 1 - asVec(currentPoint, previousPoint, v1); + previousPoint !== lastPoint ? asVec(currentPoint, previousPoint, v1) : invertVec(v2, v1); asVec(currentPoint, nextPoint, v2); sinA = v1.nx * v2.ny - v1.ny * v2.nx; sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; @@ -85,7 +86,10 @@ const calcCornerArc = (previousPoint, currentPoint, nextPoint, radiusMax) => { // Additional Part : calculate start point E startX = currentPoint.x + v1.nx * lenOut; startY = currentPoint.y + v1.ny * lenOut; -} + + // Save last point to avoid recalculating vector when not needed + lastPoint = currentPoint; +}; /** From 7e99cf769d5c245b93f3642fd385e5ea5b9448e4 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 12 Jan 2024 10:39:06 +0000 Subject: [PATCH 15/21] :rotating_light: Fix lint issues --- .../renderer/base/coord-ele-math/edge-control-points.js | 8 ++++---- src/extensions/renderer/base/node-shapes.js | 2 +- src/extensions/renderer/canvas/drawing-edges.js | 2 +- src/extensions/renderer/canvas/drawing-images.js | 4 ++-- src/extensions/renderer/canvas/drawing-shapes.js | 2 +- src/round.js | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js index aaf9047d2a..7dc8a834c0 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js @@ -463,7 +463,7 @@ BRp.findTaxiPoints = function( edge, pairInfo ){ ]; } const radius = edge.pstyle( 'taxi-radius' ).value; - rs.radii = new Array( rs.segpts.length / 2 ).fill( radius ) + rs.radii = new Array( rs.segpts.length / 2 ).fill( radius ); } }; @@ -646,16 +646,16 @@ BRp.storeAllpts = function( edge ){ point, { x: rs.segpts[ i1 + 2 ] || rs.endX, y: rs.segpts[ i1 + 3 ] || rs.endY }, radius - ) + ); let v = [ point.x - corner.cx, point.y - corner.cy ]; - const factor = corner.radius / Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)) + const factor = corner.radius / Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); - v = v.map(c => c * factor) + v = v.map(c => c * factor); rs.midX = corner.cx + v[0]; rs.midY = corner.cy + v[1]; diff --git a/src/extensions/renderer/base/node-shapes.js b/src/extensions/renderer/base/node-shapes.js index a16710cd4b..65a4588932 100644 --- a/src/extensions/renderer/base/node-shapes.js +++ b/src/extensions/renderer/base/node-shapes.js @@ -118,7 +118,7 @@ BRp.generateRoundRectangle = function(){ let halfWidth = width / 2; let halfHeight = height / 2; cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius; - cornerRadius = Math.min(halfWidth, halfHeight, cornerRadius) + cornerRadius = Math.min(halfWidth, halfHeight, cornerRadius); var diam = cornerRadius * 2; // Check hBox diff --git a/src/extensions/renderer/canvas/drawing-edges.js b/src/extensions/renderer/canvas/drawing-edges.js index 37eebb3fc6..9ae841ae99 100644 --- a/src/extensions/renderer/canvas/drawing-edges.js +++ b/src/extensions/renderer/canvas/drawing-edges.js @@ -218,7 +218,7 @@ CRp.drawEdgePath = function( edge, context, pts, type ){ drawRoundCorner(context, {x: pts[i - 2], y: pts[i - 1]}, {x: pts[i], y: pts[i + 1], radius: rs.radii[ (i / 2) - 1]}, - {x: pts[i + 2], y: pts[i + 3]}, Infinity) + {x: pts[i + 2], y: pts[i + 3]}, Infinity); } context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] ); } else { diff --git a/src/extensions/renderer/canvas/drawing-images.js b/src/extensions/renderer/canvas/drawing-images.js index af343d53cd..368b7930e4 100644 --- a/src/extensions/renderer/canvas/drawing-images.js +++ b/src/extensions/renderer/canvas/drawing-images.js @@ -34,8 +34,8 @@ CRp.drawInscribedImage = function( context, img, node, index, nodeOpacity ){ var shouldClip = clip === 'node'; var imgOpacity = getIndexedStyle( node, 'background-image-opacity', 'value', index ) * nodeOpacity; var smooth = getIndexedStyle( node, 'background-image-smoothing', 'value', index ); - var cornerRadius = node.pstyle('corner-radius').value - if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue + var cornerRadius = node.pstyle('corner-radius').value; + if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue; var imgW = img.width || img.cachedW; var imgH = img.height || img.cachedH; diff --git a/src/extensions/renderer/canvas/drawing-shapes.js b/src/extensions/renderer/canvas/drawing-shapes.js index a70daa7736..3960f9fa7d 100644 --- a/src/extensions/renderer/canvas/drawing-shapes.js +++ b/src/extensions/renderer/canvas/drawing-shapes.js @@ -33,7 +33,7 @@ CRp.drawRoundPolygonPath = function( p[i] = { x: x + halfW * points[ i * 2 ], y: y + halfH * points[ i * 2 + 1 ] - } + }; } let i, p1, p2, p3, len = p.length; diff --git a/src/round.js b/src/round.js index 60ba344736..b7db3c8944 100644 --- a/src/round.js +++ b/src/round.js @@ -16,7 +16,7 @@ const asVec = function (p, pp, v) { v.nx = v.x / v.len; v.ny = v.y / v.len; v.ang = Math.atan2(v.ny, v.nx); -} +}; const invertVec = function (originalV, invertedV) { invertedV.x = originalV.x * -1; @@ -143,7 +143,7 @@ export function getRoundCorner(previousPoint, currentPoint, nextPoint, radiusMax startAngle: undefined, endAngle: undefined, counterClockwise: undefined - } + }; calcCornerArc(previousPoint, currentPoint, nextPoint, radiusMax); return { From 4221f78a04922b68dba24ec1438a5ae79adb382e Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Fri, 12 Jan 2024 11:21:26 +0000 Subject: [PATCH 16/21] :zap: Optimise vector inversion angle recalculation (atan2 average time 0.004922 vs PI average time 0.001984) --- src/round.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/round.js b/src/round.js index b7db3c8944..4e513ebc8b 100644 --- a/src/round.js +++ b/src/round.js @@ -23,7 +23,7 @@ const invertVec = function (originalV, invertedV) { invertedV.y = originalV.y * -1; invertedV.nx = originalV.nx * -1; invertedV.ny = originalV.ny * -1; - invertedV.ang = Math.atan2(invertedV.ny, invertedV.nx); + invertedV.ang = originalV.ang > 0 ? -(Math.PI - originalV.ang): Math.PI + originalV.ang; }; const calcCornerArc = (previousPoint, currentPoint, nextPoint, radiusMax) => { From 530ee3e8559888b6779fe5545eb4d26903c05066 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Wed, 17 Jan 2024 22:08:40 +0000 Subject: [PATCH 17/21] :sparkles: Introduce node caching + support outline + include test for outline + support corner-radius impact on cut-rectangle + fix outline for hexagons --- debug/init.js | 20 +++- debug/tests.js | 18 ++++ .../renderer/base/coord-ele-math/coords.js | 3 +- .../coord-ele-math/edge-control-points.js | 17 ++-- .../base/coord-ele-math/edge-endpoints.js | 10 +- src/extensions/renderer/base/node-shapes.js | 66 ++++++++++--- .../renderer/canvas/drawing-images.js | 4 +- .../renderer/canvas/drawing-nodes.js | 94 ++++++++++++++----- .../renderer/canvas/drawing-shapes.js | 34 +------ src/extensions/renderer/canvas/node-shapes.js | 10 +- src/math.js | 57 ++--------- src/style/properties.js | 8 +- 12 files changed, 201 insertions(+), 140 deletions(-) diff --git a/debug/init.js b/debug/init.js index 31337d9977..07309f5ce2 100644 --- a/debug/init.js +++ b/debug/init.js @@ -9,7 +9,16 @@ var cy, defaultSty, options; .selector('node') .style({ 'background-opacity': 0.4, - 'label': 'data(id)' + 'label': 'data(id)', + 'outline-offset': 5, + 'outline-color': 'red', + 'outline-opacity': 0.5, + 'outline-width': 10, + 'outline-style': 'solid', + 'border-width': 5, + 'border-opacity': 0.5, + 'border-color': 'blue', + 'border-position': 'outside' }) .selector('node#a') .style({ @@ -21,9 +30,16 @@ var cy, defaultSty, options; .selector('node#b') .style({ 'shape': 'round-hexagon', - 'width': 40, + 'width': 60, + 'height': 60, 'corner-radius': 10 }) + .selector('node#e') + .style({ + 'shape': 'cut-rectangle', + 'width': 50, + 'corner-radius': 10, + }) .selector('edge') .style({ diff --git a/debug/tests.js b/debug/tests.js index cff2c6a5f7..ad598b30dc 100644 --- a/debug/tests.js +++ b/debug/tests.js @@ -94,6 +94,24 @@ } }); + test({ + name: 'outlines', + displayName: 'Outlines on all shapes', + description: 'Load an example network', + setup: function(){ + cy.elements().remove(); + const rounds = ['triangle', 'rectangle', 'diamond', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'tag'] + const shapes = ['ellipse', 'bottom-round-rectangle', 'cut-rectangle', 'barrel', 'rhomboid', 'right-rhomboid', 'concave-hexagon', 'star', 'vee', ...rounds, ...rounds.map(l => 'round-' + l)] + shapes.forEach((shape, i) => { + cy.add({data: { id: i, weight: 50 }}).style({shape}) + }) + + cy.layout({ name: 'grid' }).run(); + + cy.fit(); + } + }); + test({ name: "randomEdgeColors", displayName: "Random edge colours", diff --git a/src/extensions/renderer/base/coord-ele-math/coords.js b/src/extensions/renderer/base/coord-ele-math/coords.js index b724e42511..e46f997f0e 100644 --- a/src/extensions/renderer/base/coord-ele-math/coords.js +++ b/src/extensions/renderer/base/coord-ele-math/coords.js @@ -135,6 +135,7 @@ BRp.findNearestElements = function( x, y, interactiveElementsOnly, isTouch ){ var hh = height / 2; var pos = node.position(); var cornerRadius = node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue; + var rs = node._private.rscratch; if( pos.x - hw <= x && x <= pos.x + hw // bb check x @@ -144,7 +145,7 @@ BRp.findNearestElements = function( x, y, interactiveElementsOnly, isTouch ){ var shape = r.nodeShapes[ self.getNodeShape( node ) ]; if( - shape.checkPoint( x, y, 0, width, height, pos.x, pos.y, cornerRadius ) + shape.checkPoint( x, y, 0, width, height, pos.x, pos.y, cornerRadius, rs ) ){ addEle( node, 0 ); return true; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js index 7dc8a834c0..9e637c30e1 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-control-points.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-control-points.js @@ -472,7 +472,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ // can only correct beziers for now... if( rs.edgeType === 'bezier' ){ - const { srcPos, tgtPos, srcW, srcH, tgtW, tgtH, srcShape, tgtShape, srcCornerRadius, tgtCornerRadius } = pairInfo; + const { srcPos, tgtPos, srcW, srcH, tgtW, tgtH, srcShape, tgtShape, srcCornerRadius, tgtCornerRadius, srcRs, tgtRs } = pairInfo; let badStart = !is.number( rs.startX ) || !is.number( rs.startY ); let badAStart = !is.number( rs.arrowStartX ) || !is.number( rs.arrowStartY ); @@ -518,7 +518,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ srcH, cpProj.x, cpProj.y, - 0, srcCornerRadius + 0, srcCornerRadius, srcRs ); if( closeStartACp ){ @@ -557,7 +557,7 @@ BRp.tryToCorrectInvalidPoints = function( edge, pairInfo ){ tgtH, cpProj.x, cpProj.y, - 0, tgtCornerRadius + 0, tgtCornerRadius, tgtRs ); if( closeEndACp ){ @@ -803,6 +803,9 @@ BRp.findEdgeControlPoints = function( edges ){ let srcCornerRadius = pairInfo.srcCornerRadius = src.pstyle('corner-radius').value === 'auto' ? 'auto' : src.pstyle('corner-radius').pfValue; let tgtCornerRadius = pairInfo.tgtCornerRadius = tgt.pstyle('corner-radius').value === 'auto' ? 'auto' : tgt.pstyle('corner-radius').pfValue; + let tgtRs = pairInfo.tgtRs = tgt._private.rscratch; + let srcRs = pairInfo.srcRs = src._private.rscratch; + pairInfo.dirCounts = { 'north': 0, 'west': 0, @@ -831,7 +834,7 @@ BRp.findEdgeControlPoints = function( edges ){ srcPos.x, srcPos.y, srcW, srcH, tgtPos.x, tgtPos.y, - 0, srcCornerRadius + 0, srcCornerRadius, srcRs ); let srcIntn = pairInfo.srcIntn = srcOutside; @@ -841,7 +844,7 @@ BRp.findEdgeControlPoints = function( edges ){ tgtPos.x, tgtPos.y, tgtW, tgtH, srcPos.x, srcPos.y, - 0, tgtCornerRadius + 0, tgtCornerRadius, tgtRs ); let tgtIntn = pairInfo.tgtIntn = tgtOutside; @@ -882,8 +885,8 @@ BRp.findEdgeControlPoints = function( edges ){ // if node shapes overlap, then no ctrl pts to draw pairInfo.nodesOverlap = ( !is.number(l) - || tgtShape.checkPoint( srcOutside[0], srcOutside[1], 0, tgtW, tgtH, tgtPos.x, tgtPos.y ) - || srcShape.checkPoint( tgtOutside[0], tgtOutside[1], 0, srcW, srcH, srcPos.x, srcPos.y ) + || tgtShape.checkPoint( srcOutside[0], srcOutside[1], 0, tgtW, tgtH, tgtPos.x, tgtPos.y, tgtCornerRadius, tgtRs ) + || srcShape.checkPoint( tgtOutside[0], tgtOutside[1], 0, srcW, srcH, srcPos.x, srcPos.y, srcCornerRadius, srcRs ) ); pairInfo.vectorNormInverse = vectorNormInverse; diff --git a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js index b427536d46..951896ce30 100644 --- a/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js +++ b/src/extensions/renderer/base/coord-ele-math/edge-endpoints.js @@ -8,6 +8,7 @@ BRp.manualEndptToPx = function( node, prop ){ let npos = node.position(); let w = node.outerWidth(); let h = node.outerHeight(); + let rs = node._private.rscratch; if( prop.value.length === 2 ){ let p = [ @@ -43,7 +44,7 @@ BRp.manualEndptToPx = function( node, prop ){ npos.x, npos.y, w, h, p[0], p[1], - 0, node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue + 0, node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue, rs ); } }; @@ -64,6 +65,9 @@ BRp.findEndpoints = function( edge ){ let tgtDist = edge.pstyle( 'target-distance-from-node' ).pfValue; let srcDist = edge.pstyle( 'source-distance-from-node' ).pfValue; + let srcRs = source._private.rscratch; + let tgtRs = target._private.rscratch; + let curveStyle = edge.pstyle('curve-style').value; let rs = edge._private.rscratch; @@ -128,7 +132,7 @@ BRp.findEndpoints = function( edge ){ target.outerHeight(), p1_i[0], p1_i[1], - 0, tgtCornerRadius + 0, tgtCornerRadius, tgtRs ); if( tgtManEndptVal === 'outside-to-node-or-label' || tgtManEndptVal === 'outside-to-line-or-label' ){ @@ -220,7 +224,7 @@ BRp.findEndpoints = function( edge ){ source.outerHeight(), p2_i[0], p2_i[1], - 0, srcCornerRadius + 0, srcCornerRadius, srcRs ); if( srcManEndptVal === 'outside-to-node-or-label' || srcManEndptVal === 'outside-to-line-or-label' ){ diff --git a/src/extensions/renderer/base/node-shapes.js b/src/extensions/renderer/base/node-shapes.js index 65a4588932..dc1c7fcb3c 100644 --- a/src/extensions/renderer/base/node-shapes.js +++ b/src/extensions/renderer/base/node-shapes.js @@ -1,4 +1,5 @@ import * as math from '../../../math'; +import * as round from "../../../round"; var BRp = {}; @@ -68,24 +69,60 @@ BRp.generateRoundPolygon = function( name, points ){ points: points, - draw: function( context, centerX, centerY, width, height, cornerRadius ){ - this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points, cornerRadius ); + getOrCreateCorners: function (centerX, centerY, width, height, cornerRadius, rs, field) { + if( rs[field] !== undefined && rs[field + '-cx'] === centerX && rs [field + '-cy'] === centerY ){ + return rs[field]; + } + + rs[field] = new Array( points.length / 2 ); + rs[field + '-cx'] = centerX; + rs[field + '-cy'] = centerY; + const halfW = width / 2; + const halfH = height / 2; + cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( width, height ) : cornerRadius; + const p = new Array( points.length / 2 ); + + for ( let i = 0; i < points.length / 2; i++ ){ + p[i] = { + x: centerX + halfW * points[ i * 2 ], + y: centerY + halfH * points[ i * 2 + 1 ] + }; + } + + let i, p1, p2, p3, len = p.length; + + p1 = p[ len - 1 ]; + // for each point + for( i = 0; i < len; i++ ){ + p2 = p[ (i) % len ]; + p3 = p[ (i + 1) % len ]; + rs[ field ][ i ] = round.getRoundCorner( p1, p2, p3, cornerRadius ); + + p1 = p2; + p2 = p3; + } + + return rs[ field ]; }, - intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ + draw: function( context, centerX, centerY, width, height, cornerRadius , rs){ + this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points, this.getOrCreateCorners( centerX, centerY, width, height, cornerRadius, rs, 'drawCorners' )); + }, + + intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius, rs ){ return math.roundPolygonIntersectLine( x, y, this.points, nodeX, nodeY, width, height, - padding, cornerRadius ) + padding, this.getOrCreateCorners( nodeX, nodeY, width, height, cornerRadius, rs, 'corners' ) ) ; }, - checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ + checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius, rs ){ return math.pointInsideRoundPolygon( x, y, this.points, - centerX, centerY, width, height, cornerRadius) + centerX, centerY, width, height, this.getOrCreateCorners( centerX, centerY, width, height, cornerRadius, rs, 'corners' ) ) ; } } ); @@ -100,7 +137,7 @@ BRp.generateRoundRectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), draw: function( context, centerX, centerY, width, height, cornerRadius ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, cornerRadius ); + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, this.points, cornerRadius ); }, intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ @@ -189,11 +226,11 @@ BRp.generateCutRectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), draw: function( context, centerX, centerY, width, height, cornerRadius ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height); + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, null, cornerRadius); }, - generateCutTrianglePts: function( width, height, centerX, centerY ){ - var cl = this.cornerLength; + generateCutTrianglePts: function( width, height, centerX, centerY, cornerRadius ){ + var cl = cornerRadius === 'auto' ? this.cornerLength : cornerRadius; var hh = height / 2; var hw = width / 2; var xBegin = centerX - hw; @@ -211,7 +248,7 @@ BRp.generateCutRectangle = function(){ }, intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ - var cPts = this.generateCutTrianglePts( width + 2*padding, height+2*padding, nodeX, nodeY ); + var cPts = this.generateCutTrianglePts( width + 2*padding, height+2*padding, nodeX, nodeY, cornerRadius ); var pts = [].concat.apply([], [cPts.topLeft.splice(0, 4), cPts.topRight.splice(0, 4), cPts.bottomRight.splice(0, 4), cPts.bottomLeft.splice(0, 4) @@ -221,15 +258,16 @@ BRp.generateCutRectangle = function(){ }, checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){ + const cl = cornerRadius === 'auto' ? this.cornerLength : cornerRadius; // Check hBox if( math.pointInsidePolygon( x, y, this.points, - centerX, centerY, width, height - 2 * this.cornerLength, [0, -1], padding ) ){ + centerX, centerY, width, height - 2 * cl, [0, -1], padding ) ){ return true; } // Check vBox if( math.pointInsidePolygon( x, y, this.points, - centerX, centerY, width - 2 * this.cornerLength, height, [0, -1], padding ) ){ + centerX, centerY, width - 2 * cl, height, [0, -1], padding ) ){ return true; } var cutTrianglePts = this.generateCutTrianglePts(width, height, centerX, centerY); @@ -400,7 +438,7 @@ BRp.generateBottomRoundrectangle = function(){ points: math.generateUnitNgonPointsFitToSquare( 4, 0 ), draw: function( context, centerX, centerY, width, height, cornerRadius ){ - this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, cornerRadius ); + this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, this.points, cornerRadius ); }, intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){ diff --git a/src/extensions/renderer/canvas/drawing-images.js b/src/extensions/renderer/canvas/drawing-images.js index 368b7930e4..42adceb1f8 100644 --- a/src/extensions/renderer/canvas/drawing-images.js +++ b/src/extensions/renderer/canvas/drawing-images.js @@ -155,7 +155,7 @@ CRp.drawInscribedImage = function( context, img, node, index, nodeOpacity ){ context, nodeX, nodeY, nodeTW, nodeTH, - cornerRadius ); + cornerRadius, rs ); context.clip(); } @@ -173,7 +173,7 @@ CRp.drawInscribedImage = function( context, img, node, index, nodeOpacity ){ r.nodeShapes[ r.getNodeShape( node ) ].draw( context, nodeX, nodeY, - nodeTW, nodeTH ); + nodeTW, nodeTH, cornerRadius, rs); context.translate( x, y ); context.fill(); diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 5a671b1150..14ff05a9b4 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -1,8 +1,10 @@ /* global Path2D */ import * as is from '../../../is'; -import { expandPolygon, joinLines } from '../../../math'; +import {expandPolygon, joinLines} from '../../../math'; import * as util from '../../../util'; +import * as round from "../../../round"; +import * as math from "../../../math"; let CRp = {}; @@ -159,7 +161,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s npos.x, npos.y, nodeWidth, - nodeHeight, cornerRadius ); + nodeHeight, cornerRadius, rs ); } if( usePaths ){ @@ -205,7 +207,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s pos.x, pos.y, nodeWidth, - nodeHeight ); + nodeHeight, cornerRadius, rs ); } } } @@ -328,8 +330,12 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let shape = r.getNodeShape( node ); - let scaleX = (nodeWidth + borderWidth + (outlineWidth + outlineOffset)) / nodeWidth; - let scaleY = (nodeHeight + borderWidth + (outlineWidth + outlineOffset)) / nodeHeight; + let bWidth = borderWidth; + if( borderPosition === 'inside' ) bWidth = 0; + if( borderPosition === 'outside' ) bWidth *= 2; + + let scaleX = (nodeWidth + bWidth + (outlineWidth + outlineOffset)) / nodeWidth; + let scaleY = (nodeHeight + bWidth + (outlineWidth + outlineOffset)) / nodeHeight; let sWidth = nodeWidth * scaleX; let sHeight = nodeHeight * scaleY; @@ -352,44 +358,80 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let sMult = 0; let offsetX = 0; let offsetY = 0; + if (shape === 'round-diamond') { - sMult = (borderWidth + outlineOffset + outlineWidth) * 1.4; + sMult = (bWidth + outlineOffset + outlineWidth) * 1.4; } else if (shape === 'round-heptagon') { - sMult = (borderWidth + outlineOffset + outlineWidth) * 1.075; - offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 35; + sMult = (bWidth + outlineOffset + outlineWidth) * 1.075; + offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 35; } else if (shape === 'round-hexagon') { - sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12; + sMult = (bWidth + outlineOffset + outlineWidth) * 1.12; } else if (shape === 'round-pentagon') { - sMult = (borderWidth + outlineOffset + outlineWidth) * 1.13; - offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 15; + sMult = (bWidth + outlineOffset + outlineWidth) * 1.13; + offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 15; } else if (shape === 'round-tag') { - sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12; - offsetX = (borderWidth/2 + outlineWidth + outlineOffset) * .07; + sMult = (bWidth + outlineOffset + outlineWidth) * 1.12; + offsetX = (bWidth/2 + outlineWidth + outlineOffset) * .07; } else if (shape === 'round-triangle') { - sMult = (borderWidth + outlineOffset + outlineWidth) * (Math.PI/2); - offsetY = -(borderWidth + outlineOffset/2 + outlineWidth) / Math.PI; + sMult = (bWidth + outlineOffset + outlineWidth) * (Math.PI/2); + offsetY = -(bWidth + outlineOffset/2 + outlineWidth) / Math.PI; } if (sMult !== 0) { scaleX = (nodeWidth + sMult)/nodeWidth; - scaleY = (nodeHeight + sMult)/nodeHeight; + sWidth = nodeWidth * scaleX; + if ( ! ['round-hexagon', 'round-tag'].includes(shape) ) { + scaleY = (nodeHeight + sMult)/nodeHeight; + sHeight = nodeHeight * scaleY; + } } - r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, cornerRadius + outlineOffset); + + cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius; + + const halfW = sWidth / 2; + const halfH = sHeight / 2; + const radius = cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2; + const p = new Array( points.length / 2 ); + const corners = new Array( points.length / 2 ); + + for ( let i = 0; i < points.length / 2; i++ ){ + p[i] = { + x: npos.x + offsetX + halfW * points[ i * 2 ], + y: npos.y + offsetY + halfH * points[ i * 2 + 1 ] + }; + } + + let i, p1, p2, p3, len = p.length; + + p1 = p[ len - 1 ]; + // for each point + for( i = 0; i < len; i++ ){ + p2 = p[ (i) % len ]; + p3 = p[ (i + 1) % len ]; + corners[ i ] = round.getRoundCorner( p1, p2, p3, radius ); + p1 = p2; + p2 = p3; + } + + r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners ); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { - r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + outlineOffset); + cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; + r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { - r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); + cornerRadius = cornerRadius === 'auto' ? math.getCutRectangleCornerLength( sWidth, sHeight ) : cornerRadius; + r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, null ,cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 4 ); } else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) { - r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + outlineOffset); + cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; + r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); } else if (shape === "barrel") { r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) { - let pad = (borderWidth + outlineWidth + outlineOffset) / nodeWidth; + let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth; points = joinLines(expandPolygon(points, pad)); r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); } else { - let pad = (borderWidth + outlineWidth + outlineOffset) / nodeWidth; + let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth; points = joinLines(expandPolygon(points, -pad)); r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); } @@ -401,7 +443,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } if( outlineStyle === 'double' ){ - context.lineWidth = borderWidth / 3; + context.lineWidth = bWidth / 3; let gco = context.globalCompositeOperation; context.globalCompositeOperation = 'destination-out'; @@ -512,7 +554,8 @@ const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) { let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue; let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value; let color = node.pstyle( `${overlayOrUnderlay}-color` ).value; - var shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value; + let shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value; + let radius = node.pstyle( `${overlayOrUnderlay}-corner-radius` ).value; if( opacity > 0 ){ pos = pos || node.position(); @@ -531,7 +574,8 @@ const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) { pos.x, pos.y, nodeWidth + padding * 2, - nodeHeight + padding * 2 + nodeHeight + padding * 2, + radius ); context.fill(); diff --git a/src/extensions/renderer/canvas/drawing-shapes.js b/src/extensions/renderer/canvas/drawing-shapes.js index 3960f9fa7d..ba77120c84 100644 --- a/src/extensions/renderer/canvas/drawing-shapes.js +++ b/src/extensions/renderer/canvas/drawing-shapes.js @@ -22,34 +22,8 @@ CRp.drawPolygonPath = function( }; CRp.drawRoundPolygonPath = function( - context, x, y, width, height, points, radius ){ - - const halfW = width / 2; - const halfH = height / 2; - const cornerRadius = radius === 'auto' ? math.getRoundPolygonRadius( width, height ) : radius; - const p = new Array( points.length / 2 ); - - for ( let i = 0; i < points.length / 2; i++ ){ - p[i] = { - x: x + halfW * points[ i * 2 ], - y: y + halfH * points[ i * 2 + 1 ] - }; - } - - let i, p1, p2, p3, len = p.length; - - p1 = p[ len - 1 ]; - // for each point - for( i = 0; i < len; i++ ){ - p2 = p[ (i) % len ]; - p3 = p[ (i + 1) % len ]; - - let corner = round.getRoundCorner( p1, p2, p3, cornerRadius ); - round.drawPreparedRoundCorner( context, corner ); - - p1 = p2; - p2 = p3; - } + context, x, y, width, height, points, corners ){ + corners.forEach( corner => round.drawPreparedRoundCorner( context, corner ) ); context.closePath(); }; @@ -104,11 +78,11 @@ CRp.drawBottomRoundRectanglePath = function( }; CRp.drawCutRectanglePath = function( - context, x, y, width, height ){ + context, x, y, width, height, points, corners ){ var halfWidth = width / 2; var halfHeight = height / 2; - var cornerLength = math.getCutRectangleCornerLength(); + var cornerLength = corners === 'auto' ? math.getCutRectangleCornerLength() : corners; if( context.beginPath ){ context.beginPath(); } diff --git a/src/extensions/renderer/canvas/node-shapes.js b/src/extensions/renderer/canvas/node-shapes.js index 7a82b1b2a0..8e832b1e27 100644 --- a/src/extensions/renderer/canvas/node-shapes.js +++ b/src/extensions/renderer/canvas/node-shapes.js @@ -1,22 +1,22 @@ var CRp = {}; -CRp.nodeShapeImpl = function( name, context, centerX, centerY, width, height, points, cornerRadius ){ +CRp.nodeShapeImpl = function( name, context, centerX, centerY, width, height, points, corners ){ switch( name ){ case 'ellipse': return this.drawEllipsePath( context, centerX, centerY, width, height ); case 'polygon': return this.drawPolygonPath( context, centerX, centerY, width, height, points ); case 'round-polygon': - return this.drawRoundPolygonPath(context, centerX, centerY, width, height, points, cornerRadius ); + return this.drawRoundPolygonPath(context, centerX, centerY, width, height, points, corners ); case 'roundrectangle': case 'round-rectangle': - return this.drawRoundRectanglePath( context, centerX, centerY, width, height, points, cornerRadius ); + return this.drawRoundRectanglePath( context, centerX, centerY, width, height, corners ); case 'cutrectangle': case 'cut-rectangle': - return this.drawCutRectanglePath( context, centerX, centerY, width, height ); + return this.drawCutRectanglePath( context, centerX, centerY, width, height, points, corners ); case 'bottomroundrectangle': case 'bottom-round-rectangle': - return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height, cornerRadius ); + return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height, corners ); // TODO change case 'barrel': return this.drawBarrelPath( context, centerX, centerY, width, height ); } diff --git a/src/math.js b/src/math.js index 566b854528..4a8df191f1 100644 --- a/src/math.js +++ b/src/math.js @@ -1,5 +1,3 @@ -import * as round from "./round"; - export const arePositionsSame = ( p1, p2 ) => p1.x === p2.x && p1.y === p2.y; @@ -812,41 +810,20 @@ export const pointInsidePolygon = ( x, y, basePoints, centerX, centerY, width, h return pointInsidePolygonPoints( x, y, points ); }; -export const pointInsideRoundPolygon = (x, y, basePoints, centerX, centerY, width, height, radius = 'auto') => { +export const pointInsideRoundPolygon = (x, y, basePoints, centerX, centerY, width, height, corners) => { const cutPolygonPoints = new Array( basePoints.length * 2 ); - const halfW = width / 2; - const halfH = height / 2; - const cornerRadius = radius === 'auto' ? getRoundPolygonRadius( width, height ) : radius; - const p = new Array( basePoints.length / 2 ); - - for( let i = 0; 2 * i + 1 < basePoints.length; i++ ){ - p[ i ] = { - x: x + halfW * basePoints[ 2 * i ], - y: y + halfH * basePoints[ 2 * i + 1 ] - }; - } - - let i, p1, p2, p3, len = p.length; - - p1 = p[len - 1]; - // for each point - for (i = 0; i < len; i++) { - p2 = p[(i) % len]; - p3 = p[(i + 1) % len]; - let corner = round.getRoundCorner(p1, p2, p3, cornerRadius); + for( let i = 0; i < corners.length; i++ ){ + let corner = corners[i]; cutPolygonPoints[i * 4 + 0] = corner.startX; cutPolygonPoints[i * 4 + 1] = corner.startY; cutPolygonPoints[i * 4 + 2] = corner.stopX; cutPolygonPoints[i * 4 + 3] = corner.stopY; - const squaredDistance = Math.pow(corner.cx - x, 2) + Math.pow(corner.cy - y, 2); - if (squaredDistance <= Math.pow(corner.radius, 2)) { + const squaredDistance = Math.pow(corner.cx - x, 2 ) + Math.pow(corner.cy - y, 2 ); + if( squaredDistance <= Math.pow( corner.radius, 2 ) ){ return true; } - - p1 = p2; - p2 = p3; } return pointInsidePolygonPoints(x, y, cutPolygonPoints); @@ -1191,27 +1168,12 @@ export const polygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, return intersections; }; -export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, radius ) => { +export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, corners ) => { let intersections = []; let intersection; let lines = new Array(basePoints.length * 2); - const halfW = width / 2; - const halfH = height / 2; - const cornerRadius = radius === 'auto' ? getRoundPolygonRadius(width, height) : radius; - const p = new Array(basePoints.length / 2); - for (let i = 0; 2 * i + 1 < basePoints.length; i++) { - p[i] = {x: centerX + halfW * basePoints[2 * i], y: centerY + halfH * basePoints[2 * i + 1]}; - } - - let i, p1, p2, p3, len = p.length; - - p1 = p[len - 1]; - // for each point - for (i = 0; i < len; i++) { - p2 = p[(i) % len]; - p3 = p[(i + 1) % len]; - let corner = round.getRoundCorner(p1, p2, p3, cornerRadius); + corners.forEach( (corner, i) => { if (i === 0) { lines[lines.length - 2] = corner.startX; lines[lines.length - 1] = corner.startY; @@ -1228,10 +1190,7 @@ export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, w if (intersection.length !== 0) { intersections.push(intersection[0], intersection[1]); } - - p1 = p2; - p2 = p3; - } + }); for( let i = 0; i < lines.length / 4; i++ ) { intersection = finiteLinesIntersect( diff --git a/src/style/properties.js b/src/style/properties.js index daeebe5069..a9b3b6214c 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -253,14 +253,16 @@ const styfn = {}; { name: 'overlay-padding', type: t.size, triggersBounds: diff.any }, { name: 'overlay-color', type: t.color }, { name: 'overlay-opacity', type: t.zeroOneNumber, triggersBounds: diff.zeroNonZero }, - { name: 'overlay-shape', type: t.overlayShape, triggersBounds: diff.any } + { name: 'overlay-shape', type: t.overlayShape, triggersBounds: diff.any }, + { name: 'overlay-corner-radius', type: t.cornerRadius } ]; let underlay = [ { name: 'underlay-padding', type: t.size, triggersBounds: diff.any }, { name: 'underlay-color', type: t.color }, { name: 'underlay-opacity', type: t.zeroOneNumber, triggersBounds: diff.zeroNonZero }, - { name: 'underlay-shape', type: t.overlayShape, triggersBounds: diff.any } + { name: 'underlay-shape', type: t.overlayShape, triggersBounds: diff.any }, + { name: 'underlay-corner-radius', type: t.cornerRadius } ]; let transition = [ @@ -615,10 +617,12 @@ styfn.getDefaultProperties = function(){ 'overlay-color': '#000', 'overlay-padding': 10, 'overlay-shape': 'round-rectangle', + 'overlay-corner-radius': 'auto', 'underlay-opacity': 0, 'underlay-color': '#000', 'underlay-padding': 10, 'underlay-shape': 'round-rectangle', + 'underlay-corner-radius': 'auto', 'transition-property': 'none', 'transition-duration': 0, 'transition-delay': 0, From d2b66b30c77ed0f50f4f8aa7677faf11591ee1de Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Wed, 17 Jan 2024 22:10:44 +0000 Subject: [PATCH 18/21] :memo: Include corner-radius cut-rectangle support in documentation --- documentation/md/style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/md/style.md b/documentation/md/style.md index c9e7d39a6b..6304d580bb 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -193,7 +193,7 @@ Shape: * `vee` * `polygon` (custom polygon specified via `shape-polygon-points`). * **`shape-polygon-points`** : An array (or a space-separated string) of numbers ranging on [-1, 1], representing alternating x and y values (i.e. `x1 y1 x2 y2, x3 y3 ...`). This represents the points in the polygon for the node's shape. The bounding box of the node is given by (-1, -1), (1, -1), (1, 1), (-1, 1). The node's position is the origin (0, 0). - * **`corner-radius`** : The corner radius for `round-` shapes, in px or em. + * **`corner-radius`** : The corner radius for `round-` shapes and the `cut-rectangle`, in px or em. * **WARNING** If you are using corner radius for a parent node (see [compound nodes](#notation/compound-nodes)), you can have children nodes going outside their parent, e.g. node **E** in [compound demo](demos/compound-nodes). * In order to fix this issue, you can play with the `padding` of the parent node. Having the same value for `padding` and `corner-radius` is always safe, e.g. node **B** in [compound demo](demos/compound-nodes). From 3013eefb51f2aa0f2b1fcb778490ed1a6976a1bd Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Thu, 18 Jan 2024 17:02:39 +0000 Subject: [PATCH 19/21] :bug: Avoid caching issue with different corner-radii --- debug/init.js | 17 +++++++++++++---- src/extensions/renderer/canvas/drawing-nodes.js | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/debug/init.js b/debug/init.js index 07309f5ce2..3c1c4339da 100644 --- a/debug/init.js +++ b/debug/init.js @@ -18,14 +18,16 @@ var cy, defaultSty, options; 'border-width': 5, 'border-opacity': 0.5, 'border-color': 'blue', - 'border-position': 'outside' + 'border-position': 'inside', + 'width': 220, + 'height': 60 }) .selector('node#a') .style({ 'shape': 'round-rectangle', - 'width': 100, - 'height': 50, - 'corner-radius': 25 + 'width': 220, + 'height': 60, + 'corner-radius': 30 }) .selector('node#b') .style({ @@ -34,6 +36,13 @@ var cy, defaultSty, options; 'height': 60, 'corner-radius': 10 }) + .selector('node#c') + .style({ + 'shape': 'round-rectangle', + 'width': 220, + 'height': 60, + 'corner-radius': 5 + }) .selector('node#e') .style({ 'shape': 'cut-rectangle', diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 14ff05a9b4..f5ac47a011 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -111,7 +111,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let key = util.hashStrings( shape === 'polygon' ? shape + ',' + points.join(',') : shape, '' + height, - '' + width + '' + width, + '' + cornerRadius ); let cachedPath = pathCache[ key ]; @@ -416,6 +417,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners ); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { + console.log(cornerRadius, node) cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { From 7c54ec14f9c48b01d5ec13e83167406cf88c9e42 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Thu, 18 Jan 2024 17:10:07 +0000 Subject: [PATCH 20/21] :mute: Remove logs --- src/extensions/renderer/canvas/drawing-nodes.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index f5ac47a011..0b817096fe 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -387,7 +387,6 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } } - cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius; const halfW = sWidth / 2; @@ -417,7 +416,6 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners ); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { - console.log(cornerRadius, node) cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { From d8941d169c9304af844ecb7a701fb23f93f368a4 Mon Sep 17 00:00:00 2001 From: EliotRagueneau Date: Thu, 18 Jan 2024 17:11:08 +0000 Subject: [PATCH 21/21] :wastebasket: Remove TODO --- src/extensions/renderer/canvas/node-shapes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/renderer/canvas/node-shapes.js b/src/extensions/renderer/canvas/node-shapes.js index 8e832b1e27..37a77bfa1f 100644 --- a/src/extensions/renderer/canvas/node-shapes.js +++ b/src/extensions/renderer/canvas/node-shapes.js @@ -16,7 +16,7 @@ CRp.nodeShapeImpl = function( name, context, centerX, centerY, width, height, po return this.drawCutRectanglePath( context, centerX, centerY, width, height, points, corners ); case 'bottomroundrectangle': case 'bottom-round-rectangle': - return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height, corners ); // TODO change + return this.drawBottomRoundRectanglePath( context, centerX, centerY, width, height, corners ); case 'barrel': return this.drawBarrelPath( context, centerX, centerY, width, height ); }