Skip to content

Commit

Permalink
Merge pull request #3158 from kumu/node-outlines
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkfranz authored Nov 10, 2023
2 parents dcc3778 + d1c0103 commit f0c24be
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 19 deletions.
8 changes: 8 additions & 0 deletions documentation/md/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
Expand Down
53 changes: 52 additions & 1 deletion src/collection/dimensions/bounds.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -428,6 +428,52 @@ 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;
let outlineWidth = ele.pstyle('outline-width').value;

if (outlineOpacity > 0 && outlineWidth > 0) {
let outlineOffset = ele.pstyle('outline-offset').value;
let nodeShape = ele.pstyle( 'shape' ).value;

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", "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 = (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") {
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, [Math.ceil(hDelta/2), Math.ceil(wDelta/2)]);

if (xOffset != 0 || yOffset !== 0) {
let oBounds = shiftBoundingBox(bounds, xOffset, yOffset);
updateBoundingBox(bounds, oBounds);
}
}
};

// get the bounding box of the elements (in raw model position)
let boundingBoxImpl = function( ele, options ){
let cy = ele._private.cy;
Expand Down Expand Up @@ -510,6 +556,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 ){
Expand Down Expand Up @@ -735,6 +784,7 @@ let getKey = function( opts ){
key += tf( opts.includeSourceLabels );
key += tf( opts.includeTargetLabels );
key += tf( opts.includeOverlays );
key += tf( opts.includeOutlines );

return key;
};
Expand Down Expand Up @@ -824,6 +874,7 @@ let defBbOpts = {
includeTargetLabels: true,
includeOverlays: true,
includeUnderlays: true,
includeOutlines: true,
useCache: true
};

Expand Down
184 changes: 169 additions & 15 deletions src/extensions/renderer/canvas/drawing-nodes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global Path2D */

import * as is from '../../../is';
import { expandPolygon, joinLines } from '../../../math';
import * as util from '../../../util';

let CRp = {};
Expand Down Expand Up @@ -74,6 +75,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').pfValue;
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

Expand All @@ -85,33 +91,50 @@ 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

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 drawShape = () => {
Expand All @@ -127,11 +150,11 @@ 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 );
}

if( usePaths ){
Expand Down Expand Up @@ -246,7 +269,133 @@ 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 npos = pos;

if( usePaths ){
npos = {
x: 0,
y: 0
};
}

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;

if (usePaths) {
let outlinePath = getPath(sWidth, sHeight, shape, points);
path = outlinePath.path;
}

// 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)) {
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)) {
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.stroke( path );
} else {
context.stroke();
}

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
context.setLineDash( [ ] );
}
}
};

Expand Down Expand Up @@ -276,6 +425,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s

context.translate( gx, gy );

setupOutlineColor();
drawOutline();
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
Expand All @@ -296,13 +447,16 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
context.translate( pos.x, pos.y );
}

setupOutlineColor();
drawOutline();
setupShapeColor();
drawShape();
drawImages(eleOpacity, true);
setupBorderColor();
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawImages(eleOpacity, false);

darken();

if( usePaths ){
Expand Down
4 changes: 2 additions & 2 deletions src/style/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
]);
Expand Down
Loading

0 comments on commit f0c24be

Please sign in to comment.