Canvas: Match connection anchor points to elements (#85421)

Co-authored-by: Ihor Yeromin <yeryomin.igor@gmail.com>
This commit is contained in:
Nathan Marrs 2024-04-23 15:26:53 -06:00 committed by GitHub
parent fc5007b0d5
commit 5dea949433
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 148 additions and 29 deletions

View File

@ -101,6 +101,12 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
/** Optional config to customize what standard element editor options are available for the item */
standardEditorConfig?: StandardEditorConfig;
/** Custom connection anchor coordinates, like for svg elements such as triangle, cloud, etc */
customConnectionAnchors?: Array<{
x: number;
y: number;
}>;
}
export const defaultBgColor = '#D9D9D9';

View File

@ -184,6 +184,24 @@ export const cloudItem: CanvasElementItem = {
},
});
},
customConnectionAnchors: [
{ x: -0.58, y: 0.63 }, // Top Left
{ x: -0.22, y: 0.99 }, // Top Middle
{ x: 0.235, y: 0.75 }, // Top Right
{ x: 0.8, y: 0.6 }, // Right Top
{ x: 0.785, y: 0.06 }, // Right Middle
{ x: 0.91, y: -0.51 }, // Right Bottom
{ x: 0.62, y: -0.635 }, // Bottom Right
{ x: 0.05, y: -0.98 }, // Bottom Middle
{ x: -0.45, y: -0.635 }, // Bottom Left
{ x: -0.8, y: -0.58 }, // Left Bottom
{ x: -0.78, y: -0.06 }, // Left Middle
{ x: -0.9, y: 0.48 }, // Left Top
],
};
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {

View File

@ -190,6 +190,24 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
},
});
},
customConnectionAnchors: [
// points along the left edge
{ x: -1, y: 0 }, // middle left
{ x: -0.7, y: 0.7 },
// points along the top edge
{ x: 0, y: 1 }, // top
{ x: 0.7, y: 0.7 },
// points along the right edge
{ x: 1, y: 0 }, // middle right
{ x: 0.7, y: -0.7 },
// points along the bottom edge
{ x: 0, y: -1 }, // bottom
{ x: -0.7, y: -0.7 },
],
};
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {

View File

@ -184,6 +184,25 @@ export const parallelogramItem: CanvasElementItem = {
},
});
},
customConnectionAnchors: [
{ x: -0.6, y: 1 }, // Angled Top Left
{ x: -0.1, y: 1 }, // Top Middle
{ x: 0.5, y: 1 }, // Angled Top Right
{ x: 1, y: 1 }, // Top Right
{ x: 0.925, y: 0.6 }, // Angled Right Top
{ x: 0.84, y: 0.2 }, // Right Middle
{ x: 0.76, y: -0.2 }, // Angled Right Bottom
{ x: 0.675, y: -0.6 }, // Bottom Right
{ x: -0.5, y: -1 }, // Angled Bottom Right
{ x: 0.1, y: -1 }, // Bottom Middle
{ x: 0.6, y: -1 }, // Angled Bottom Left
{ x: -1, y: -1 }, // Bottom Left
{ x: -0.925, y: -0.6 }, // Angled Left Bottom
{ x: -0.84, y: -0.2 }, // Left Middle
{ x: -0.76, y: 0.2 }, // Angled Left Top
{ x: -0.675, y: 0.6 }, // Top Left 2
],
};
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {

View File

@ -185,6 +185,29 @@ export const triangleItem: CanvasElementItem = {
},
});
},
customConnectionAnchors: [
// points along the left edge
{ x: -1, y: -1 }, // bottom left
{ x: -0.8, y: -0.6 },
{ x: -0.6, y: -0.2 },
{ x: -0.4, y: 0.2 },
{ x: -0.2, y: 0.6 },
{ x: 0, y: 1 }, // top
// points along the right edge
{ x: 0.2, y: 0.6 },
{ x: 0.4, y: 0.2 },
{ x: 0.6, y: -0.2 },
{ x: 0.8, y: -0.6 },
{ x: 1, y: -1 }, // bottom right
// points along the bottom edge
{ x: 0.6, y: -1 },
{ x: 0.2, y: -1 },
{ x: -0.2, y: -1 },
{ x: -0.6, y: -1 },
],
};
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {

View File

@ -7,6 +7,7 @@ import { ConnectionCoordinates } from 'app/features/canvas';
type Props = {
setRef: (anchorElement: HTMLDivElement) => void;
setAnchorsRef: (anchorsElement: HTMLDivElement) => void;
handleMouseLeave: (
event: React.MouseEvent<Element, MouseEvent> | React.FocusEvent<HTMLDivElement, Element>
) => boolean;
@ -15,13 +16,33 @@ type Props = {
export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl';
export const CONNECTION_ANCHOR_ALT = 'connection anchor';
export const CONNECTION_ANCHOR_HIGHLIGHT_OFFSET = 8;
// Unit is percentage from the middle of the element
// 0, 0 middle; -1, -1 bottom left; 1, 1 top right
export const ANCHORS = [
{ x: -1, y: 1 },
{ x: -0.5, y: 1 },
{ x: 0, y: 1 },
{ x: 0.5, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 0.5 },
{ x: 1, y: 0 },
{ x: 1, y: -0.5 },
{ x: 1, y: -1 },
{ x: 0.5, y: -1 },
{ x: 0, y: -1 },
{ x: -0.5, y: -1 },
{ x: -1, y: -1 },
{ x: -1, y: -0.5 },
{ x: -1, y: 0 },
{ x: -1, y: 0.5 },
];
const ANCHOR_PADDING = 3;
export const ANCHOR_PADDING = 3;
export const HALF_SIZE = 2.5;
export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => {
export const ConnectionAnchors = ({ setRef, setAnchorsRef, handleMouseLeave }: Props) => {
const highlightEllipseRef = useRef<HTMLDivElement>(null);
const styles = useStyles2(getStyles);
const halfSize = 2.5;
const halfSizeHighlightEllipse = 5.5;
const anchorImage =
'data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1cHgiIGhlaWdodD0iNXB4IiB2ZXJzaW9uPSIxLjEiPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2Utb3BhY2l0eTowLjQiIHN0cm9rZT0iI2ZmZmZmZiIvPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2U9IiMyOWI2ZjIiLz48L3N2Zz4=';
@ -54,35 +75,14 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => {
}
};
// Unit is percentage from the middle of the element
// 0, 0 middle; -1, -1 bottom left; 1, 1 top right
const ANCHORS = [
{ x: -1, y: 1 },
{ x: -0.5, y: 1 },
{ x: 0, y: 1 },
{ x: 0.5, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 0.5 },
{ x: 1, y: 0 },
{ x: 1, y: -0.5 },
{ x: 1, y: -1 },
{ x: 0.5, y: -1 },
{ x: 0, y: -1 },
{ x: -0.5, y: -1 },
{ x: -1, y: -1 },
{ x: -1, y: -0.5 },
{ x: -1, y: 0 },
{ x: -1, y: 0.5 },
];
const generateAnchors = (anchors: ConnectionCoordinates[] = ANCHORS) => {
return anchors.map((anchor) => {
const id = `${anchor.x},${anchor.y}`;
// Convert anchor coords to relative percentage
const style = {
top: `calc(${-anchor.y * 50 + 50}% - ${halfSize}px - ${ANCHOR_PADDING}px)`,
left: `calc(${anchor.x * 50 + 50}% - ${halfSize}px - ${ANCHOR_PADDING}px)`,
top: `calc(${-anchor.y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`,
left: `calc(${anchor.x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`,
};
return (
@ -114,7 +114,7 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => {
className={styles.highlightElement}
onMouseLeave={onMouseLeaveHighlightElement}
/>
{generateAnchors()}
<div ref={setAnchorsRef}>{generateAnchors()}</div>
</div>
);
};

View File

@ -16,7 +16,14 @@ import {
isConnectionTarget,
} from '../../utils';
import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors';
import {
CONNECTION_ANCHOR_ALT,
ConnectionAnchors,
CONNECTION_ANCHOR_HIGHLIGHT_OFFSET,
ANCHORS,
ANCHOR_PADDING,
HALF_SIZE,
} from './ConnectionAnchors';
import { ConnectionSVG } from './ConnectionSVG';
export const CONNECTION_VERTEX_ID = 'vertex';
@ -27,6 +34,7 @@ const CONNECTION_VERTEX_SNAP_TOLERANCE = (5 / 180) * Math.PI; // Multi-segment s
export class Connections {
scene: Scene;
connectionAnchorDiv?: HTMLDivElement;
anchorsDiv?: HTMLDivElement;
connectionSVG?: SVGElement;
connectionLine?: SVGLineElement;
connectionSVGVertex?: SVGElement;
@ -70,6 +78,10 @@ export class Connections {
this.connectionAnchorDiv = anchorElement;
};
setAnchorsRef = (anchorsElement: HTMLDivElement) => {
this.anchorsDiv = anchorsElement;
};
setConnectionSVGRef = (connectionSVG: SVGSVGElement) => {
this.connectionSVG = connectionSVG;
};
@ -130,6 +142,25 @@ export class Connections {
}
}
const customElementAnchors = element?.item.customConnectionAnchors || ANCHORS;
// This type cast is necessary as TS doesn't understand that `Element` is an `HTMLElement`
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const anchors = Array.from(this.anchorsDiv?.children as HTMLCollectionOf<HTMLElement>);
const anchorsAmount = customElementAnchors.length;
// re-calculate the position of the existing anchors on hover
// and hide the rest of the anchors if there are more than the custom ones
anchors.forEach((anchor, index) => {
if (index >= anchorsAmount) {
anchor.style.display = 'none';
} else {
const { x, y } = customElementAnchors[index];
anchor.style.top = `calc(${-y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`;
anchor.style.left = `calc(${x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`;
anchor.style.display = 'block';
}
});
const elementBoundingRect = element.div!.getBoundingClientRect();
const transformScale = this.scene.scale;
const parentBoundingRect = getParentBoundingClientRect(this.scene);
@ -648,7 +679,11 @@ export class Connections {
render() {
return (
<>
<ConnectionAnchors setRef={this.setConnectionAnchorRef} handleMouseLeave={this.handleMouseLeave} />
<ConnectionAnchors
setRef={this.setConnectionAnchorRef}
setAnchorsRef={this.setAnchorsRef}
handleMouseLeave={this.handleMouseLeave}
/>
<ConnectionSVG
setSVGRef={this.setConnectionSVGRef}
setLineRef={this.setConnectionLineRef}