From dfdf1b778588a13a347e8f7618ed89e063a58f66 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 22 Aug 2023 11:56:43 -0400 Subject: [PATCH 01/12] generalize path caching into `getPath` method --- .../renderer/canvas/drawing-nodes.js | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 3717f60881..47d2a8f635 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -5,11 +5,46 @@ import * as util from '../../../util'; let CRp = {}; +// Returns a path for the provided node/shape/width/height +// combination, creating and caching the path when first +// requested and returning the cached version on subsequent +// requests. +CRp.getPath = function(node, shape, width, height) { + let _p = node._private; + let rs = _p.rscratch; + let pathCache = this.nodePathCache = this.nodePathCache || []; + let shapePts = node.pstyle('shape-polygon-points').pfValue; + + let key = util.hashStrings( + shape === 'polygon' ? shape + ',' + shapePts.join(',') : shape, + '' + height, + '' + width + ); + + let path; + let pathCacheHit = false; + + let cachedPath = pathCache[ key ]; + + if( cachedPath != null ){ + path = cachedPath; + pathCacheHit = true; + rs.pathCache = path; + } else { + path = new Path2D(); + pathCache[ key ] = rs.pathCache = path; + } + + return { + path, + cacheHit: pathCacheHit, + }; +}; + CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){ let r = this; let nodeWidth, nodeHeight; let _p = node._private; - let rs = _p.rscratch; let pos = node.position(); if( !is.number( pos.x ) || !is.number( pos.y ) ){ @@ -89,29 +124,13 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s // setup shape let styleShape = node.pstyle('shape').strValue; - let shapePts = node.pstyle('shape-polygon-points').pfValue; if( usePaths ){ context.translate( pos.x, pos.y ); - let pathCache = r.nodePathCache = r.nodePathCache || []; - - let key = util.hashStrings( - styleShape === 'polygon' ? styleShape + ',' + shapePts.join(',') : styleShape, - '' + nodeHeight, - '' + nodeWidth - ); - - let cachedPath = pathCache[ key ]; - - if( cachedPath != null ){ - path = cachedPath; - pathCacheHit = true; - rs.pathCache = path; - } else { - path = new Path2D(); - pathCache[ key ] = rs.pathCache = path; - } + let shapePath = r.getPath(node, styleShape, nodeWidth, nodeHeight); + pathCacheHit = shapePath.cacheHit; + path = shapePath.path; } let drawShape = () => { From 52862fb1c6230cb252e60e60045b4cfdaa41d0fa Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 22 Aug 2023 12:17:07 -0400 Subject: [PATCH 02/12] add outline style props --- src/style/properties.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/style/properties.js b/src/style/properties.js index 3804852bfd..7e9eb485fa 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -297,6 +297,14 @@ const styfn = {}; { name: 'border-style', type: t.borderStyle } ]; + let nodeOutline = [ + { name: 'outline-color', type: t.color }, + { name: 'outline-opacity', type: t.zeroOneNumber }, + { name: 'outline-width', type: t.size, triggersBounds: diff.any }, + { name: 'outline-style', type: t.borderStyle }, + { name: 'outline-offset', type: t.size, triggersBounds: diff.any }, + ]; + let backgroundImage = [ { name: 'background-image', type: t.urls }, { name: 'background-image-crossorigin', type: t.bgCrossOrigin }, @@ -421,6 +429,7 @@ const styfn = {}; // node props ...nodeBody, ...nodeBorder, + ...nodeOutline, ...backgroundImage, ...pie, ...compound, @@ -451,6 +460,7 @@ const styfn = {}; // node props nodeBody, nodeBorder, + nodeOutline, backgroundImage, pie, compound, @@ -622,6 +632,11 @@ styfn.getDefaultProperties = function(){ 'border-opacity': 1, 'border-width': 0, 'border-style': 'solid', + 'outline-color': '#999', + 'outline-opacity': 1, + 'outline-width': 0, + 'outline-style': 'solid', + 'outline-offset': 0, 'height': 30, 'width': 30, 'shape': 'ellipse', From 39be8125e6a0a8e5e99df72928a881fa11ce2c27 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 22 Aug 2023 12:31:57 -0400 Subject: [PATCH 03/12] update node rendering to draw outlines --- .../renderer/canvas/drawing-nodes.js | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 47d2a8f635..1829822207 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -109,6 +109,11 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let borderColor = node.pstyle('border-color').value; let borderStyle = node.pstyle('border-style').value; let borderOpacity = node.pstyle('border-opacity').value * eleOpacity; + let outlineWidth = node.pstyle('outline-width').value; + let outlineColor = node.pstyle('outline-color').value; + let outlineStyle = node.pstyle('outline-style').value; + let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; + let outlineOffset = node.pstyle('outline-offset').value; context.lineJoin = 'miter'; // so borders are square with the node shape @@ -120,6 +125,10 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy ); }; + let setupOutlineColor = ( otlnOpy = outlineOpacity ) => { + r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy ); + }; + // // setup shape @@ -265,7 +274,99 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s if( context.setLineDash ){ // for very outofdate browsers context.setLineDash( [ ] ); } + } + }; + + let drawOutline = () => { + if( outlineWidth > 0 ){ + context.lineWidth = outlineWidth; + context.lineCap = 'butt'; + + if( context.setLineDash ){ // for very outofdate browsers + switch( outlineStyle ){ + case 'dotted': + context.setLineDash( [ 1, 1 ] ); + break; + + case 'dashed': + context.setLineDash( [ 4, 2 ] ); + break; + + case 'solid': + case 'double': + context.setLineDash( [ ] ); + break; + } + } + + let width = nodeWidth + outlineWidth + outlineOffset; + let height = nodeHeight + outlineWidth + outlineOffset; + + if (borderWidth > 0) { + width += borderWidth; + height += borderWidth; + } + + let shape = r.getNodeShape( node ); + let npos = pos; + + if ( usePaths ) { + npos = { + x: 0, + y: 0, + }; + } + + let { path, cacheHit } = r.getPath( + node, + shape, + width, + height + ); + + if ( usePaths ){ + if( !cacheHit ){ + r.nodeShapes[shape].draw( + path, + npos.x, + npos.y, + width, + height + ); + } + + context.stroke( path ); + } else { + r.nodeShapes[shape].draw( + context, + npos.x, + npos.y, + width, + height + ); + + context.stroke(); + } + + if( outlineStyle === 'double' ){ + context.lineWidth = outlineWidth / 3; + + let gco = context.globalCompositeOperation; + context.globalCompositeOperation = 'destination-out'; + + if( usePaths ){ + context.stroke( path ); + } else { + context.stroke(); + } + + context.globalCompositeOperation = gco; + } + // reset in case we changed the outline style + if( context.setLineDash ){ // for very outofdate browsers + context.setLineDash( [ ] ); + } } }; @@ -295,6 +396,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s context.translate( gx, gy ); + setupOutlineColor(); + drawOutline(); setupShapeColor( ghostOpacity * bgOpacity ); drawShape(); drawImages( effGhostOpacity, true ); @@ -315,6 +418,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s context.translate( pos.x, pos.y ); } + setupOutlineColor(); + drawOutline(); setupShapeColor(); drawShape(); drawImages(eleOpacity, true); From 38f58f57162a59e57087e1a304ea6f71a9860e00 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 22 Aug 2023 12:39:43 -0400 Subject: [PATCH 04/12] update node cache key to include outline styles --- src/style/apply.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/style/apply.js b/src/style/apply.js index b731f5b4a2..18debf1aba 100644 --- a/src/style/apply.js +++ b/src/style/apply.js @@ -357,9 +357,9 @@ styfn.updateStyleHints = function(ele){ // if( isNode ){ - let { nodeBody, nodeBorder, backgroundImage, compound, pie } = _p.styleKeys; + let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie } = _p.styleKeys; - let nodeKeys = [ nodeBody, nodeBorder, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [ + let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ]); From 3218d258349e0838a6f3106f1d2282349228c6a7 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 22 Aug 2023 12:49:14 -0400 Subject: [PATCH 05/12] update docs --- documentation/md/style.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/md/style.md b/documentation/md/style.md index 312fc628e0..0f7a50c7b3 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -223,6 +223,14 @@ Border: * **`border-color`** : The colour of the node's border. * **`border-opacity`** : The opacity of the node's border. + Outline: + + * **`outline-width`** : The size of the node's outline. + * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, `dashed`, or `double`. + * **`outline-color`** : The colour of the node's outline. + * **`outline-opacity`** : The opacity of the node's outline. + * **`outline-offset`** : The offset of the node's outline, measured in percent (e.g. `50%`) or pixels (e.g. `10px`). + Padding: A padding defines an addition to a node's dimension. For example, `padding` adds to a node's outer (i.e. total) width and height. This can be used to add spacing between a compound node parent and its children. From ac3b23cc88b4cf3e67404489f11276923590be8a Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Mon, 11 Sep 2023 10:12:32 +0300 Subject: [PATCH 06/12] remove `outline-offset` property --- documentation/md/style.md | 1 - src/extensions/renderer/canvas/drawing-nodes.js | 1 - src/style/properties.js | 2 -- 3 files changed, 4 deletions(-) diff --git a/documentation/md/style.md b/documentation/md/style.md index 0f7a50c7b3..e0cf4f1645 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -229,7 +229,6 @@ Border: * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, `dashed`, or `double`. * **`outline-color`** : The colour of the node's outline. * **`outline-opacity`** : The opacity of the node's outline. - * **`outline-offset`** : The offset of the node's outline, measured in percent (e.g. `50%`) or pixels (e.g. `10px`). Padding: diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 1829822207..f4b141d115 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -113,7 +113,6 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let outlineColor = node.pstyle('outline-color').value; let outlineStyle = node.pstyle('outline-style').value; let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; - let outlineOffset = node.pstyle('outline-offset').value; context.lineJoin = 'miter'; // so borders are square with the node shape diff --git a/src/style/properties.js b/src/style/properties.js index 7e9eb485fa..78a3c2bd22 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -302,7 +302,6 @@ const styfn = {}; { name: 'outline-opacity', type: t.zeroOneNumber }, { name: 'outline-width', type: t.size, triggersBounds: diff.any }, { name: 'outline-style', type: t.borderStyle }, - { name: 'outline-offset', type: t.size, triggersBounds: diff.any }, ]; let backgroundImage = [ @@ -636,7 +635,6 @@ styfn.getDefaultProperties = function(){ 'outline-opacity': 1, 'outline-width': 0, 'outline-style': 'solid', - 'outline-offset': 0, 'height': 30, 'width': 30, 'shape': 'ellipse', From ded390e1d0ca968d044c7a5104d923f1239f259b Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Mon, 11 Sep 2023 10:14:18 +0300 Subject: [PATCH 07/12] remove support for `double` outline style --- documentation/md/style.md | 2 +- src/extensions/renderer/canvas/drawing-nodes.js | 1 - src/style/properties.js | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/md/style.md b/documentation/md/style.md index e0cf4f1645..5ed4e34b3b 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -226,7 +226,7 @@ Border: Outline: * **`outline-width`** : The size of the node's outline. - * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, `dashed`, or `double`. + * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, or `dashed`. * **`outline-color`** : The colour of the node's outline. * **`outline-opacity`** : The opacity of the node's outline. diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index f4b141d115..88d28acd45 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -292,7 +292,6 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s break; case 'solid': - case 'double': context.setLineDash( [ ] ); break; } diff --git a/src/style/properties.js b/src/style/properties.js index 78a3c2bd22..c88970fe37 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -142,7 +142,8 @@ const styfn = {}; return length === 1 || length === 2 || length === 4; } - } + }, + outlineStyle: { enums: [ 'solid', 'dotted', 'dashed' ] }, }; let diff = { @@ -301,7 +302,7 @@ const styfn = {}; { name: 'outline-color', type: t.color }, { name: 'outline-opacity', type: t.zeroOneNumber }, { name: 'outline-width', type: t.size, triggersBounds: diff.any }, - { name: 'outline-style', type: t.borderStyle }, + { name: 'outline-style', type: t.outlineStyle }, ]; let backgroundImage = [ From f3010889fce146e7ebbd7215f216504b884ce0d0 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Tue, 12 Sep 2023 09:58:48 +0300 Subject: [PATCH 08/12] simplify outline rendering --- src/collection/dimensions/bounds.js | 29 ++++ .../renderer/canvas/drawing-nodes.js | 152 ++++++------------ 2 files changed, 77 insertions(+), 104 deletions(-) diff --git a/src/collection/dimensions/bounds.js b/src/collection/dimensions/bounds.js index 31aae6e0bc..fa7b8fabfc 100644 --- a/src/collection/dimensions/bounds.js +++ b/src/collection/dimensions/bounds.js @@ -428,6 +428,31 @@ let updateBoundsFromLabel = function( bounds, ele, prefix ){ return bounds; }; +let updateBoundsFromOutline = function( bounds, ele ){ + if( ele.cy().headless() ){ return; } + + let outlineOpacity = ele.pstyle('outline-opacity').value; + + if (outlineOpacity > 0) { + let outlineWidth = ele.pstyle( 'outline-width' ).pfValue; + let shape = ele.pstyle('shape').strValue; + let size = outlineWidth * 2; + + if (shape === "vee" || shape === "triangle") { + size *= 1.5; + } + + let ex1 = bounds.x1 - size; + let ex2 = bounds.x2 + size; + let ey1 = bounds.y1 - size; + let ey2 = bounds.y2 + size; + + updateBounds(bounds, ex1, ey1, ex2, ey2); + } + + return bounds; +}; + // get the bounding box of the elements (in raw model position) let boundingBoxImpl = function( ele, options ){ let cy = ele._private.cy; @@ -510,6 +535,9 @@ let boundingBoxImpl = function( ele, options ){ updateBounds( bounds, ex1, ey1, ex2, ey2 ); + if( styleEnabled && options.includeOutlines ){ + updateBoundsFromOutline( bounds, ele ); + } } else if( isEdge && options.includeEdges ){ if( styleEnabled && !headless ){ @@ -824,6 +852,7 @@ let defBbOpts = { includeTargetLabels: true, includeOverlays: true, includeUnderlays: true, + includeOutlines: true, useCache: true }; diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 88d28acd45..26623bad94 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -5,46 +5,11 @@ import * as util from '../../../util'; let CRp = {}; -// Returns a path for the provided node/shape/width/height -// combination, creating and caching the path when first -// requested and returning the cached version on subsequent -// requests. -CRp.getPath = function(node, shape, width, height) { - let _p = node._private; - let rs = _p.rscratch; - let pathCache = this.nodePathCache = this.nodePathCache || []; - let shapePts = node.pstyle('shape-polygon-points').pfValue; - - let key = util.hashStrings( - shape === 'polygon' ? shape + ',' + shapePts.join(',') : shape, - '' + height, - '' + width - ); - - let path; - let pathCacheHit = false; - - let cachedPath = pathCache[ key ]; - - if( cachedPath != null ){ - path = cachedPath; - pathCacheHit = true; - rs.pathCache = path; - } else { - path = new Path2D(); - pathCache[ key ] = rs.pathCache = path; - } - - return { - path, - cacheHit: pathCacheHit, - }; -}; - CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){ let r = this; let nodeWidth, nodeHeight; let _p = node._private; + let rs = _p.rscratch; let pos = node.position(); if( !is.number( pos.x ) || !is.number( pos.y ) ){ @@ -109,7 +74,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let borderColor = node.pstyle('border-color').value; let borderStyle = node.pstyle('border-style').value; let borderOpacity = node.pstyle('border-opacity').value * eleOpacity; - let outlineWidth = node.pstyle('outline-width').value; + let outlineWidth = node.pstyle('outline-width').pfValue; let outlineColor = node.pstyle('outline-color').value; let outlineStyle = node.pstyle('outline-style').value; let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; @@ -132,16 +97,32 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s // setup shape let styleShape = node.pstyle('shape').strValue; + let shapePts = node.pstyle('shape-polygon-points').pfValue; if( usePaths ){ context.translate( pos.x, pos.y ); - let shapePath = r.getPath(node, styleShape, nodeWidth, nodeHeight); - pathCacheHit = shapePath.cacheHit; - path = shapePath.path; + let pathCache = r.nodePathCache = r.nodePathCache || []; + + let key = util.hashStrings( + styleShape === 'polygon' ? styleShape + ',' + shapePts.join(',') : styleShape, + '' + nodeHeight, + '' + nodeWidth + ); + + let cachedPath = pathCache[ key ]; + + if( cachedPath != null ){ + path = cachedPath; + pathCacheHit = true; + rs.pathCache = path; + } else { + path = new Path2D(); + pathCache[ key ] = rs.pathCache = path; + } } - let drawShape = () => { + let drawPath = () => { if( !pathCacheHit ){ let npos = pos; @@ -154,12 +135,16 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } r.nodeShapes[ r.getNodeShape( node ) ].draw( - ( path || context ), - npos.x, - npos.y, - nodeWidth, - nodeHeight ); + ( path || context ), + npos.x, + npos.y, + nodeWidth, + nodeHeight ); } + } + + let drawShape = () => { + drawPath(); if( usePaths ){ context.fill( path ); @@ -278,7 +263,9 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let drawOutline = () => { if( outlineWidth > 0 ){ - context.lineWidth = outlineWidth; + // because outline and border are drawn along the same path, + // draw outline at border width plus twice the outline width + context.lineWidth = borderWidth + outlineWidth * 2; context.lineCap = 'butt'; if( context.setLineDash ){ // for very outofdate browsers @@ -297,71 +284,28 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s } } - let width = nodeWidth + outlineWidth + outlineOffset; - let height = nodeHeight + outlineWidth + outlineOffset; - - if (borderWidth > 0) { - width += borderWidth; - height += borderWidth; - } - - let shape = r.getNodeShape( node ); - let npos = pos; - - if ( usePaths ) { - npos = { - x: 0, - y: 0, - }; - } - - let { path, cacheHit } = r.getPath( - node, - shape, - width, - height - ); - - if ( usePaths ){ - if( !cacheHit ){ - r.nodeShapes[shape].draw( - path, - npos.x, - npos.y, - width, - height - ); - } - + drawPath(); + + if( usePaths ){ context.stroke( path ); } else { - r.nodeShapes[shape].draw( - context, - npos.x, - npos.y, - width, - height - ); - context.stroke(); } - if( outlineStyle === 'double' ){ - context.lineWidth = outlineWidth / 3; - - let gco = context.globalCompositeOperation; - context.globalCompositeOperation = 'destination-out'; - - if( usePaths ){ - context.stroke( path ); - } else { - context.stroke(); - } + const gco = context.globalCompositeOperation; + context.globalCompositeOperation = 'destination-out'; - context.globalCompositeOperation = gco; + r.eleFillStyle( context, node, 1 ); + + if( usePaths ){ + context.fill( path ); + } else { + context.fill(); } - // reset in case we changed the outline style + context.globalCompositeOperation = gco; + + // reset in case we changed the border style if( context.setLineDash ){ // for very outofdate browsers context.setLineDash( [ ] ); } From ed2c095bdf4df790e9bc51037e72f77b9776b041 Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Wed, 18 Oct 2023 17:19:13 +0300 Subject: [PATCH 09/12] draw outlines by expanding polygons --- documentation/md/style.md | 3 +- src/collection/dimensions/bounds.js | 33 +++-- .../renderer/canvas/drawing-nodes.js | 119 +++++++++++++----- src/style/properties.js | 5 +- 4 files changed, 110 insertions(+), 50 deletions(-) diff --git a/documentation/md/style.md b/documentation/md/style.md index 5ed4e34b3b..dea69bfd5f 100644 --- a/documentation/md/style.md +++ b/documentation/md/style.md @@ -226,9 +226,10 @@ Border: Outline: * **`outline-width`** : The size of the node's outline. - * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, or `dashed`. + * **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, `dashed`, or `double`. * **`outline-color`** : The colour of the node's outline. * **`outline-opacity`** : The opacity of the node's outline. + * **`outline-offset`** : The offset of the node's outline. Padding: diff --git a/src/collection/dimensions/bounds.js b/src/collection/dimensions/bounds.js index fa7b8fabfc..7e18eecded 100644 --- a/src/collection/dimensions/bounds.js +++ b/src/collection/dimensions/bounds.js @@ -432,25 +432,21 @@ let updateBoundsFromOutline = function( bounds, ele ){ if( ele.cy().headless() ){ return; } let outlineOpacity = ele.pstyle('outline-opacity').value; - - if (outlineOpacity > 0) { - let outlineWidth = ele.pstyle( 'outline-width' ).pfValue; - let shape = ele.pstyle('shape').strValue; - let size = outlineWidth * 2; - - if (shape === "vee" || shape === "triangle") { - size *= 1.5; - } - - let ex1 = bounds.x1 - size; - let ex2 = bounds.x2 + size; - let ey1 = bounds.y1 - size; - let ey2 = bounds.y2 + size; - - updateBounds(bounds, ex1, ey1, ex2, ey2); + let outlineWidth = ele.pstyle('outline-width').value; + + if (outlineOpacity > 0 && outlineWidth > 0) { + let outlineOffset = ele.pstyle('outline-offset').value; + // apply an multiplier to arrive at a bounding box that + // exceeds the outline in the absence of a more accurate + // way to calculate bounds + let size = (outlineWidth + outlineOffset) * 3; + let x1 = bounds.x1 - size; + let y1 = bounds.y1 - size; + let x2 = bounds.x2 + size; + let y2 = bounds.y2 + size; + + updateBounds( bounds, x1, y1, x2, y2 ); } - - return bounds; }; // get the bounding box of the elements (in raw model position) @@ -763,6 +759,7 @@ let getKey = function( opts ){ key += tf( opts.includeSourceLabels ); key += tf( opts.includeTargetLabels ); key += tf( opts.includeOverlays ); + key += tf( opts.includeOutlines ); return key; }; diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index 26623bad94..e44ef48bc5 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -1,6 +1,7 @@ /* global Path2D */ import * as is from '../../../is'; +import { expandPolygon, joinLines } from '../../../math'; import * as util from '../../../util'; let CRp = {}; @@ -78,6 +79,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let outlineColor = node.pstyle('outline-color').value; let outlineStyle = node.pstyle('outline-style').value; let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; + let outlineOffset = node.pstyle('outline-offset').value; context.lineJoin = 'miter'; // so borders are square with the node shape @@ -96,33 +98,46 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s // // setup shape - let styleShape = node.pstyle('shape').strValue; - let shapePts = node.pstyle('shape-polygon-points').pfValue; - - if( usePaths ){ - context.translate( pos.x, pos.y ); - + let getPath = (width, height, shape, points) => { let pathCache = r.nodePathCache = r.nodePathCache || []; let key = util.hashStrings( - styleShape === 'polygon' ? styleShape + ',' + shapePts.join(',') : styleShape, - '' + nodeHeight, - '' + nodeWidth + shape === 'polygon' ? shape + ',' + points.join(',') : shape, + '' + height, + '' + width ); let cachedPath = pathCache[ key ]; + let path; + let cacheHit = false; if( cachedPath != null ){ path = cachedPath; - pathCacheHit = true; + cacheHit = true; rs.pathCache = path; } else { path = new Path2D(); pathCache[ key ] = rs.pathCache = path; } + + return { + path, + cacheHit + }; + }; + + let styleShape = node.pstyle('shape').strValue; + let shapePts = node.pstyle('shape-polygon-points').pfValue; + + if( usePaths ){ + context.translate( pos.x, pos.y ); + + const shapePath = getPath(nodeWidth, nodeHeight, styleShape, shapePts); + path = shapePath.path; + pathCacheHit = shapePath.cacheHit; } - let drawPath = () => { + let drawShape = () => { if( !pathCacheHit ){ let npos = pos; @@ -141,10 +156,6 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s nodeWidth, nodeHeight ); } - } - - let drawShape = () => { - drawPath(); if( usePaths ){ context.fill( path ); @@ -263,9 +274,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let drawOutline = () => { if( outlineWidth > 0 ){ - // because outline and border are drawn along the same path, - // draw outline at border width plus twice the outline width - context.lineWidth = borderWidth + outlineWidth * 2; + context.lineWidth = outlineWidth; context.lineCap = 'butt'; if( context.setLineDash ){ // for very outofdate browsers @@ -279,31 +288,82 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s break; case 'solid': + case 'double': context.setLineDash( [ ] ); break; } } - drawPath(); - + let npos = pos; + if( usePaths ){ - context.stroke( path ); - } else { - context.stroke(); + npos = { + x: 0, + y: 0 + }; } - const gco = context.globalCompositeOperation; - context.globalCompositeOperation = 'destination-out'; + let shape = r.getNodeShape( node ); + + let scale = (nodeWidth + borderWidth + (outlineWidth + outlineOffset)) / nodeWidth; + let sWidth = nodeWidth * scale; + let sHeight = nodeHeight * scale; + + let points = r.nodeShapes[ shape ].points; + let path; + + if (usePaths) { + let outlinePath = getPath(sWidth, sHeight, shape, points); + path = outlinePath.path; + } - r.eleFillStyle( context, node, 1 ); + // draw the outline path, either by using expanded points or by scaling + // the dimensions, depending on shape + if (shape === "ellipse") { + r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight); + } else if ([ + 'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon', + 'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag' + ].includes(shape)) { + r.drawRoundPolygonPath(path || context, npos.x, npos.y, sWidth, sHeight, points); + } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { + r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); + } 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); + } 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; + points = joinLines(expandPolygon(points, pad)); + r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); + } else { + let pad = (borderWidth + outlineWidth + outlineOffset) / nodeWidth; + points = joinLines(expandPolygon(points, -pad)); + r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); + } if( usePaths ){ - context.fill( path ); + context.stroke( path ); } else { - context.fill(); + context.stroke(); } - context.globalCompositeOperation = gco; + if( outlineStyle === 'double' ){ + context.lineWidth = borderWidth / 3; + + let gco = context.globalCompositeOperation; + context.globalCompositeOperation = 'destination-out'; + + if( usePaths ){ + context.stroke( path ); + } else { + context.stroke(); + } + + context.globalCompositeOperation = gco; + } // reset in case we changed the border style if( context.setLineDash ){ // for very outofdate browsers @@ -369,6 +429,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s drawBorder(); drawPie( darkness !== 0 || borderWidth !== 0 ); drawImages(eleOpacity, false); + darken(); if( usePaths ){ diff --git a/src/style/properties.js b/src/style/properties.js index c88970fe37..86b907c7cb 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -143,7 +143,6 @@ const styfn = {}; return length === 1 || length === 2 || length === 4; } }, - outlineStyle: { enums: [ 'solid', 'dotted', 'dashed' ] }, }; let diff = { @@ -302,7 +301,8 @@ const styfn = {}; { name: 'outline-color', type: t.color }, { name: 'outline-opacity', type: t.zeroOneNumber }, { name: 'outline-width', type: t.size, triggersBounds: diff.any }, - { name: 'outline-style', type: t.outlineStyle }, + { name: 'outline-style', type: t.borderStyle }, + { name: 'outline-offset', type: t.size }, ]; let backgroundImage = [ @@ -635,6 +635,7 @@ styfn.getDefaultProperties = function(){ 'outline-color': '#999', 'outline-opacity': 1, 'outline-width': 0, + 'outline-offset': 0, 'outline-style': 'solid', 'height': 30, 'width': 30, From e66b1fee948f2a9211d5743f440e47e9c6ab6a6a Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Thu, 19 Oct 2023 10:05:21 +0300 Subject: [PATCH 10/12] tweak round shape outlines, bounds calc --- src/collection/dimensions/bounds.js | 44 ++++++++++++++----- .../renderer/canvas/drawing-nodes.js | 35 +++++++++++++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/collection/dimensions/bounds.js b/src/collection/dimensions/bounds.js index 7e18eecded..dfaf008b4f 100644 --- a/src/collection/dimensions/bounds.js +++ b/src/collection/dimensions/bounds.js @@ -1,5 +1,5 @@ import * as is from '../../is'; -import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox } from '../../math'; +import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox, shiftBoundingBox, updateBoundingBox } from '../../math'; import { defaults, getPrefixedProperty, hashIntsArray } from '../../util'; let fn, elesfn; @@ -436,16 +436,38 @@ let updateBoundsFromOutline = function( bounds, ele ){ if (outlineOpacity > 0 && outlineWidth > 0) { let outlineOffset = ele.pstyle('outline-offset').value; - // apply an multiplier to arrive at a bounding box that - // exceeds the outline in the absence of a more accurate - // way to calculate bounds - let size = (outlineWidth + outlineOffset) * 3; - let x1 = bounds.x1 - size; - let y1 = bounds.y1 - size; - let x2 = bounds.x2 + size; - let y2 = bounds.y2 + size; - - updateBounds( bounds, x1, y1, x2, y2 ); + let nodeShape = ele.pstyle( 'shape' ).value; + + let scaleX = (bounds.w + (outlineWidth + outlineOffset)) / bounds.w; + let scaleY = (bounds.h + (outlineWidth + outlineOffset)) / bounds.h; + let xOffset = 0; + let yOffset = 0; + + if (["diamond", "pentagon", "polygon", "round-triangle", "star", "triangle"].includes(nodeShape)) { + scaleX = (bounds.w + (outlineWidth + outlineOffset) * 2) / bounds.w; + scaleY = (bounds.h + (outlineWidth + outlineOffset) * 2) / bounds.h; + } else if (["concave-hexagon", "rhomboid", "right-rhomboid"].includes(nodeShape)) { + scaleX *= 1.2; + } else if (nodeShape === "tag") { + scaleX *= 1.15; + } else if (nodeShape === "triangle") { + yOffset = -(outlineWidth + outlineOffset); + } + + if (nodeShape === "vee") { + scaleX = (bounds.w + (outlineWidth + outlineOffset) * 2.5) / bounds.w; + scaleY = (bounds.h + (outlineWidth + outlineOffset) * 2.5) / bounds.h; + yOffset = -(outlineWidth + outlineOffset); + } + + let hDelta = (bounds.h * scaleY) - bounds.h; + let wDelta = (bounds.w * scaleX) - bounds.w; + expandBoundingBoxSides(bounds, [hDelta, wDelta]); + + if (xOffset != 0 || yOffset !== 0) { + let oBounds = shiftBoundingBox(bounds, xOffset, yOffset); + updateBoundingBox(bounds, oBounds); + } } }; diff --git a/src/extensions/renderer/canvas/drawing-nodes.js b/src/extensions/renderer/canvas/drawing-nodes.js index e44ef48bc5..2f4122d24b 100644 --- a/src/extensions/renderer/canvas/drawing-nodes.js +++ b/src/extensions/renderer/canvas/drawing-nodes.js @@ -305,9 +305,10 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s let shape = r.getNodeShape( node ); - let scale = (nodeWidth + borderWidth + (outlineWidth + outlineOffset)) / nodeWidth; - let sWidth = nodeWidth * scale; - let sHeight = nodeHeight * scale; + 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 +326,33 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s 'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon', 'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag' ].includes(shape)) { - r.drawRoundPolygonPath(path || context, npos.x, npos.y, sWidth, sHeight, points); + let sMult = 0; + let offsetX = 0; + let offsetY = 0; + if (shape === 'round-diamond') { + sMult = (borderWidth + outlineOffset + outlineWidth) * 1.4; + } else if (shape === 'round-heptagon') { + sMult = (borderWidth + outlineOffset + outlineWidth) * 1.075; + offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 35; + } else if (shape === 'round-hexagon') { + sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12; + } else if (shape === 'round-pentagon') { + sMult = (borderWidth + outlineOffset + outlineWidth) * 1.13; + offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 15; + } else if (shape === 'round-tag') { + sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12; + offsetX = (borderWidth/2 + outlineWidth + outlineOffset) * .07; + } else if (shape === 'round-triangle') { + sMult = (borderWidth + outlineOffset + outlineWidth) * (Math.PI/2); + offsetY = -(borderWidth + outlineOffset/2 + outlineWidth) / Math.PI; + } + + if (sMult !== 0) { + scaleX = (nodeWidth + sMult)/nodeWidth; + scaleY = (nodeHeight + sMult)/nodeHeight; + } + + r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points); } else if (['roundrectangle', 'round-rectangle'].includes(shape)) { r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight); } else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { From b245cc7db566cb16294e13af464175bd6a54962e Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Fri, 27 Oct 2023 16:49:40 +0100 Subject: [PATCH 11/12] tighten up bounds calculations --- src/collection/dimensions/bounds.js | 35 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/collection/dimensions/bounds.js b/src/collection/dimensions/bounds.js index dfaf008b4f..35c82a83b0 100644 --- a/src/collection/dimensions/bounds.js +++ b/src/collection/dimensions/bounds.js @@ -438,31 +438,34 @@ let updateBoundsFromOutline = function( bounds, ele ){ let outlineOffset = ele.pstyle('outline-offset').value; let nodeShape = ele.pstyle( 'shape' ).value; - let scaleX = (bounds.w + (outlineWidth + outlineOffset)) / bounds.w; - let scaleY = (bounds.h + (outlineWidth + outlineOffset)) / bounds.h; + let outlineSize = outlineWidth + outlineOffset; + let scaleX = (bounds.w + outlineSize * 2) / bounds.w; + let scaleY = (bounds.h + outlineSize * 2) / bounds.h; let xOffset = 0; let yOffset = 0; - if (["diamond", "pentagon", "polygon", "round-triangle", "star", "triangle"].includes(nodeShape)) { - scaleX = (bounds.w + (outlineWidth + outlineOffset) * 2) / bounds.w; - scaleY = (bounds.h + (outlineWidth + outlineOffset) * 2) / bounds.h; + if (["diamond", "pentagon", "round-triangle"].includes(nodeShape)) { + scaleX = (bounds.w + outlineSize * 2.4) / bounds.w; + yOffset = -outlineSize/3.6; } else if (["concave-hexagon", "rhomboid", "right-rhomboid"].includes(nodeShape)) { - scaleX *= 1.2; - } else if (nodeShape === "tag") { - scaleX *= 1.15; + scaleX = (bounds.w + outlineSize * 2.4) / bounds.w; + } else if (nodeShape === "star") { + scaleX = (bounds.w + outlineSize * 2.8) / bounds.w; + scaleY = (bounds.h + outlineSize * 2.6) / bounds.h; + yOffset = -outlineSize / 3.8; } else if (nodeShape === "triangle") { - yOffset = -(outlineWidth + outlineOffset); - } - - if (nodeShape === "vee") { - scaleX = (bounds.w + (outlineWidth + outlineOffset) * 2.5) / bounds.w; - scaleY = (bounds.h + (outlineWidth + outlineOffset) * 2.5) / bounds.h; - yOffset = -(outlineWidth + outlineOffset); + scaleX = (bounds.w + outlineSize * 2.8) / bounds.w; + scaleY = (bounds.h + outlineSize * 2.4) / bounds.h; + yOffset = -outlineSize/1.4; + } else if (nodeShape === "vee") { + scaleX = (bounds.w + outlineSize * 4.4) / bounds.w; + scaleY = (bounds.h + outlineSize * 3.8) / bounds.h; + yOffset = -outlineSize * .5; } let hDelta = (bounds.h * scaleY) - bounds.h; let wDelta = (bounds.w * scaleX) - bounds.w; - expandBoundingBoxSides(bounds, [hDelta, wDelta]); + expandBoundingBoxSides(bounds, [Math.ceil(hDelta/2), Math.ceil(wDelta/2)]); if (xOffset != 0 || yOffset !== 0) { let oBounds = shiftBoundingBox(bounds, xOffset, yOffset); From d1c010309125395618f7021a2f68ce448efd456c Mon Sep 17 00:00:00 2001 From: Noah Pedrini Date: Fri, 27 Oct 2023 17:13:56 +0100 Subject: [PATCH 12/12] update `outline-offset` prop to trigger bounds --- src/style/properties.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style/properties.js b/src/style/properties.js index 86b907c7cb..93bc945525 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -302,7 +302,7 @@ const styfn = {}; { name: 'outline-opacity', type: t.zeroOneNumber }, { name: 'outline-width', type: t.size, triggersBounds: diff.any }, { name: 'outline-style', type: t.borderStyle }, - { name: 'outline-offset', type: t.size }, + { name: 'outline-offset', type: t.size, triggersBounds: diff.any } ]; let backgroundImage = [