mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Add corner radius option (#84873)
* Canvas: Add corner radius option * Update connection radius logic * Simplify angle calcs * Simplify math to be a bit more clear and efficient * Add checks for hyperbola behavior * Prevent arc calcs if no radius * Add comments for SOME clarity * Add some more clarity to comments * Fix linter issue * Check for segment overlap for first vertex * Update public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx * Add comment for calc clarity
This commit is contained in:
parent
863a3d1c2c
commit
0b4830ccfd
@ -58,6 +58,7 @@ export interface BackgroundConfig {
|
||||
|
||||
export interface LineConfig {
|
||||
color?: ui.ColorDimensionConfig;
|
||||
radius?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ export interface CanvasConnection {
|
||||
size?: ScaleDimensionConfig;
|
||||
lineStyle?: string;
|
||||
vertices?: ConnectionCoordinates[];
|
||||
radius?: ScaleDimensionConfig;
|
||||
direction?: ConnectionDirection;
|
||||
// See https://github.com/anseki/leader-line#options for more examples of more properties
|
||||
}
|
||||
|
@ -358,6 +358,10 @@ export class ElementState implements LayerElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (border && border.radius !== undefined) {
|
||||
css.borderRadius = `${border.radius}px`;
|
||||
}
|
||||
|
||||
this.dataStyle = css;
|
||||
this.applyLayoutStylesToDiv();
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ import { ConnectionCoordinates } from '../../panelcfg.gen';
|
||||
import { ConnectionState } from '../../types';
|
||||
import {
|
||||
calculateAbsoluteCoords,
|
||||
calculateAngle,
|
||||
calculateCoordinates,
|
||||
calculateDistance,
|
||||
calculateMidpoint,
|
||||
getConnectionStyles,
|
||||
getParentBoundingClientRect,
|
||||
@ -135,8 +137,10 @@ export const ConnectionSVG = ({
|
||||
|
||||
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);
|
||||
const midpoint = calculateMidpoint(x1, y1, x2, y2);
|
||||
const xDist = x2 - x1;
|
||||
const yDist = y2 - y1;
|
||||
|
||||
const { strokeColor, strokeWidth, arrowDirection, lineStyle } = getConnectionStyles(
|
||||
const { strokeColor, strokeWidth, strokeRadius, arrowDirection, lineStyle } = getConnectionStyles(
|
||||
info,
|
||||
scene,
|
||||
defaultArrowSize,
|
||||
@ -151,6 +155,7 @@ export const ConnectionSVG = ({
|
||||
const CONNECTION_HEAD_ID_START = `connectionHeadStart-${headId + Math.random()}`;
|
||||
const CONNECTION_HEAD_ID_END = `connectionHeadEnd-${headId + Math.random()}`;
|
||||
|
||||
const radius = strokeRadius;
|
||||
// Create vertex path and populate array of add vertex controls
|
||||
const addVertices: ConnectionCoordinates[] = [];
|
||||
let pathString = `M${x1} ${y1} `;
|
||||
@ -158,20 +163,171 @@ export const ConnectionSVG = ({
|
||||
vertices.map((vertex, index) => {
|
||||
const x = vertex.x;
|
||||
const y = vertex.y;
|
||||
pathString += `L${x * (x2 - x1) + x1} ${y * (y2 - y1) + y1} `;
|
||||
|
||||
// Convert vertex relative coordinates to scene coordinates
|
||||
const X = x * xDist + x1;
|
||||
const Y = y * yDist + y1;
|
||||
|
||||
// Initialize coordinates for first arc control point
|
||||
let xa = X;
|
||||
let ya = Y;
|
||||
|
||||
// Initialize coordinates for second arc control point
|
||||
let xb = X;
|
||||
let yb = Y;
|
||||
|
||||
// Initialize half arc distance and segment angles
|
||||
let lHalfArc = 0;
|
||||
let angle1 = 0;
|
||||
let angle2 = 0;
|
||||
|
||||
// Only calculate arcs if there is a radius
|
||||
if (radius) {
|
||||
if (index < vertices.length - 1) {
|
||||
const Xn = vertices[index + 1].x * xDist + x1;
|
||||
const Yn = vertices[index + 1].y * yDist + y1;
|
||||
if (index === 0) {
|
||||
// First vertex
|
||||
angle1 = calculateAngle(x1, y1, X, Y);
|
||||
angle2 = calculateAngle(X, Y, Xn, Yn);
|
||||
} else {
|
||||
// All vertices
|
||||
const previousVertex = vertices[index - 1];
|
||||
const Xp = previousVertex.x * xDist + x1;
|
||||
const Yp = previousVertex.y * yDist + y1;
|
||||
angle1 = calculateAngle(Xp, Yp, X, Y);
|
||||
angle2 = calculateAngle(X, Y, Xn, Yn);
|
||||
}
|
||||
} else {
|
||||
// Last vertex
|
||||
let previousVertex = { x: 0, y: 0 };
|
||||
if (index > 0) {
|
||||
// Not also the first vertex
|
||||
previousVertex = vertices[index - 1];
|
||||
}
|
||||
const Xp = previousVertex.x * xDist + x1;
|
||||
const Yp = previousVertex.y * yDist + y1;
|
||||
angle1 = calculateAngle(Xp, Yp, X, Y);
|
||||
angle2 = calculateAngle(X, Y, x2, y2);
|
||||
}
|
||||
|
||||
// Calculate angle between two segments where arc will be placed
|
||||
const theta = angle2 - angle1; //radians
|
||||
// Attempt to determine if arc is counter clockwise (ccw)
|
||||
const ccw = theta < 0;
|
||||
// Half arc is used for arc control points
|
||||
lHalfArc = radius * Math.tan(theta / 2);
|
||||
if (ccw) {
|
||||
lHalfArc *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
// For first vertex
|
||||
addVertices.push(calculateMidpoint(0, 0, x, y));
|
||||
|
||||
// Only calculate arcs if there is a radius
|
||||
if (radius) {
|
||||
// Length of segment
|
||||
const lSegment = calculateDistance(X, Y, x1, y1);
|
||||
if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegment)) {
|
||||
// Limit curve control points to mid segment
|
||||
lHalfArc = 0.5 * lSegment;
|
||||
}
|
||||
// Default next point to last point
|
||||
let Xn = x2;
|
||||
let Yn = y2;
|
||||
if (index < vertices.length - 1) {
|
||||
// Not also the last point
|
||||
const nextVertex = vertices[index + 1];
|
||||
Xn = nextVertex.x * xDist + x1;
|
||||
Yn = nextVertex.y * yDist + y1;
|
||||
}
|
||||
|
||||
// Length of next segment
|
||||
const lSegmentNext = calculateDistance(X, Y, Xn, Yn);
|
||||
if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegmentNext)) {
|
||||
// Limit curve control points to mid segment
|
||||
lHalfArc = 0.5 * lSegmentNext;
|
||||
}
|
||||
// Calculate arc control points
|
||||
const lDelta = lSegment - lHalfArc;
|
||||
xa = lDelta * Math.cos(angle1) + x1;
|
||||
ya = lDelta * Math.sin(angle1) + y1;
|
||||
xb = lHalfArc * Math.cos(angle2) + X;
|
||||
yb = lHalfArc * Math.sin(angle2) + Y;
|
||||
|
||||
// Check if arc control points are inside of segment, otherwise swap sign
|
||||
if ((xa > X && xa > x1) || (xa < X && xa < x1)) {
|
||||
xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + x1;
|
||||
ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + y1;
|
||||
xb = -lHalfArc * Math.cos(angle2) + X;
|
||||
yb = -lHalfArc * Math.sin(angle2) + Y;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For all other vertices
|
||||
const previousVertex = vertices[index - 1];
|
||||
addVertices.push(calculateMidpoint(previousVertex.x, previousVertex.y, x, y));
|
||||
|
||||
// Only calculate arcs if there is a radius
|
||||
if (radius) {
|
||||
// Convert previous vertex relative coorindates to scene coordinates
|
||||
const Xp = previousVertex.x * xDist + x1;
|
||||
const Yp = previousVertex.y * yDist + y1;
|
||||
|
||||
// Length of segment
|
||||
const lSegment = calculateDistance(X, Y, Xp, Yp);
|
||||
if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegment)) {
|
||||
// Limit curve control points to mid segment
|
||||
lHalfArc = 0.5 * lSegment;
|
||||
}
|
||||
// Default next point to last point
|
||||
let Xn = x2;
|
||||
let Yn = y2;
|
||||
if (index < vertices.length - 1) {
|
||||
// Not also the last point
|
||||
const nextVertex = vertices[index + 1];
|
||||
Xn = nextVertex.x * xDist + x1;
|
||||
Yn = nextVertex.y * yDist + y1;
|
||||
}
|
||||
|
||||
// Length of next segment
|
||||
const lSegmentNext = calculateDistance(X, Y, Xn, Yn);
|
||||
if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegmentNext)) {
|
||||
// Limit curve control points to mid segment
|
||||
lHalfArc = 0.5 * lSegmentNext;
|
||||
}
|
||||
|
||||
// Calculate arc control points
|
||||
const lDelta = lSegment - lHalfArc;
|
||||
xa = lDelta * Math.cos(angle1) + Xp;
|
||||
ya = lDelta * Math.sin(angle1) + Yp;
|
||||
xb = lHalfArc * Math.cos(angle2) + X;
|
||||
yb = lHalfArc * Math.sin(angle2) + Y;
|
||||
|
||||
// Check if arc control points are inside of segment, otherwise swap sign
|
||||
if ((xa > X && xa > Xp) || (xa < X && xa < Xp)) {
|
||||
xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + Xp;
|
||||
ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + Yp;
|
||||
xb = -lHalfArc * Math.cos(angle2) + X;
|
||||
yb = -lHalfArc * Math.sin(angle2) + Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (index === vertices.length - 1) {
|
||||
// For last vertex
|
||||
// For last vertex only
|
||||
addVertices.push(calculateMidpoint(1, 1, x, y));
|
||||
}
|
||||
// Add segment to path
|
||||
pathString += `L${xa} ${ya} `;
|
||||
|
||||
if (lHalfArc !== 0) {
|
||||
// Add arc if applicable
|
||||
pathString += `Q ${X} ${Y} ${xb} ${yb} `;
|
||||
}
|
||||
});
|
||||
// Add last segment
|
||||
pathString += `L${x2} ${y2}`;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ import { ConnectionSVG } from './ConnectionSVG';
|
||||
export const CONNECTION_VERTEX_ID = 'vertex';
|
||||
export const CONNECTION_VERTEX_ADD_ID = 'vertexAdd';
|
||||
const CONNECTION_VERTEX_ORTHO_TOLERANCE = 0.05; // Cartesian ratio against vertical or horizontal tolerance
|
||||
const CONNECTION_VERTEX_SNAP_TOLERANCE = 5; // Multi-segment snapping angle in degrees to trigger vertex removal
|
||||
const CONNECTION_VERTEX_SNAP_TOLERANCE = (5 / 180) * Math.PI; // Multi-segment snapping angle in radians to trigger vertex removal
|
||||
|
||||
export class Connections {
|
||||
scene: Scene;
|
||||
|
@ -36,6 +36,7 @@ export function getConnectionEditor(opts: CanvasConnectionEditorOptions): Nested
|
||||
const ctx = { ...context, options: opts.connection.info };
|
||||
optionBuilder.addColor(builder, ctx);
|
||||
optionBuilder.addSize(builder, ctx);
|
||||
optionBuilder.addRadius(builder, ctx);
|
||||
optionBuilder.addDirection(builder, ctx);
|
||||
optionBuilder.addLineStyle(builder, ctx);
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ interface OptionSuppliers {
|
||||
addBorder: PanelOptionsSupplier<CanvasElementOptions>;
|
||||
addColor: PanelOptionsSupplier<CanvasConnection>;
|
||||
addSize: PanelOptionsSupplier<CanvasConnection>;
|
||||
addRadius: PanelOptionsSupplier<CanvasConnection>;
|
||||
addDirection: PanelOptionsSupplier<CanvasConnection>;
|
||||
addLineStyle: PanelOptionsSupplier<CanvasConnection>;
|
||||
}
|
||||
@ -90,6 +91,17 @@ export const optionBuilder: OptionSuppliers = {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
builder.addSliderInput({
|
||||
category,
|
||||
path: 'border.radius',
|
||||
name: 'Radius',
|
||||
defaultValue: 0,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 60,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addColor: (builder, context) => {
|
||||
@ -129,6 +141,27 @@ export const optionBuilder: OptionSuppliers = {
|
||||
});
|
||||
},
|
||||
|
||||
addRadius: (builder, context) => {
|
||||
const category = ['Radius'];
|
||||
builder.addCustomEditor({
|
||||
category,
|
||||
id: 'radius',
|
||||
path: 'radius',
|
||||
name: 'Radius',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 200,
|
||||
},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addDirection: (builder, context) => {
|
||||
const category = ['Arrow Direction'];
|
||||
builder.addRadio({
|
||||
|
@ -52,8 +52,9 @@ composableKinds: PanelCfg: {
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
LineConfig: {
|
||||
color?: ui.ColorDimensionConfig
|
||||
width?: float64
|
||||
color?: ui.ColorDimensionConfig
|
||||
width?: float64
|
||||
radius?: float64
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
HttpRequestMethod: "GET" | "POST" | "PUT" @cuetsy(kind="enum", memberNames="GET|POST|PUT")
|
||||
|
@ -56,6 +56,7 @@ export interface BackgroundConfig {
|
||||
|
||||
export interface LineConfig {
|
||||
color?: ui.ColorDimensionConfig;
|
||||
radius?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
|
@ -370,8 +370,14 @@ export const calculateAbsoluteCoords = (
|
||||
return { x: valueX * (x2 - x1) + x1, y: valueY * (y2 - y1) + y1 };
|
||||
};
|
||||
|
||||
// Calculate angle between two points and return angle in radians
|
||||
export const calculateAngle = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
|
||||
return Math.atan2(y2 - y1, x2 - x1);
|
||||
};
|
||||
|
||||
export const calculateDistance = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
//TODO add sqrt approx option
|
||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
};
|
||||
|
||||
// @TODO revisit, currently returning last row index for field
|
||||
@ -395,9 +401,10 @@ export const getConnectionStyles = (
|
||||
const lastRowIndex = getRowIndex(info.size?.field, scene);
|
||||
const strokeColor = info.color ? scene.context.getColor(info.color).value() : defaultArrowColor;
|
||||
const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize;
|
||||
const strokeRadius = info.radius ? scene.context.getScale(info.radius).get(lastRowIndex) : 0;
|
||||
const arrowDirection = info.direction ? info.direction : defaultArrowDirection;
|
||||
const lineStyle = info.lineStyle === LineStyle.Dashed ? StrokeDasharray.Dashed : StrokeDasharray.Solid;
|
||||
return { strokeColor, strokeWidth, arrowDirection, lineStyle };
|
||||
return { strokeColor, strokeWidth, strokeRadius, arrowDirection, lineStyle };
|
||||
};
|
||||
|
||||
export const getParentBoundingClientRect = (scene: Scene) => {
|
||||
|
Loading…
Reference in New Issue
Block a user