mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 08:18:10 -05:00
Canvas: Add vertex control to connections (#83653)
* Canvas: Add vertex control to connections * Add function for vertex conversion * Add vertex interface * Add future vertex handling * Only show vertices when connection selected * Add vertices to save model * Apply select constraint to first midpoint * Add some infrastructure for vertex edit * Render vertex edit and capture events * Save vertex edit on button release * Handle adding new vertices * Limit number of vertices to 10 * Handle zoom for vertex edit and creation * Rename future to add * Remove more references to future * Remove unsued console log * Clean up styles * Add some clarity for path generation * Add clarity to connections event handling --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
nmarrs
parent
6d74553653
commit
f273681956
+5
@@ -83,8 +83,13 @@ export interface CanvasConnection {
|
||||
source: ConnectionCoordinates;
|
||||
target: ConnectionCoordinates;
|
||||
targetName?: string;
|
||||
vertices?: Array<ConnectionCoordinates>;
|
||||
}
|
||||
|
||||
export const defaultCanvasConnection: Partial<CanvasConnection> = {
|
||||
vertices: [],
|
||||
};
|
||||
|
||||
export interface CanvasElementOptions {
|
||||
background?: BackgroundConfig;
|
||||
border?: LineConfig;
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CanvasConnection {
|
||||
path: ConnectionPath;
|
||||
color?: ColorDimensionConfig;
|
||||
size?: ScaleDimensionConfig;
|
||||
vertices?: ConnectionCoordinates[];
|
||||
// See https://github.com/anseki/leader-line#options for more examples of more properties
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
} from 'app/features/dimensions/utils';
|
||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/components/CanvasContextMenu';
|
||||
import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip';
|
||||
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
||||
import {
|
||||
CONNECTION_ANCHOR_DIV_ID,
|
||||
CONNECTION_VERTEX_ADD_ID,
|
||||
CONNECTION_VERTEX_ID,
|
||||
} from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
||||
import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections';
|
||||
import { AnchorPoint, CanvasTooltipPayload, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
import { getParent, getTransformInstance } from 'app/plugins/panel/canvas/utils';
|
||||
@@ -615,6 +619,20 @@ export class Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is a vertex, eject to handle vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ID) {
|
||||
this.connections.handleVertexDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// If selected target is an add vertex point, eject to handle add vertex event
|
||||
if (selectedTarget.id === CONNECTION_VERTEX_ADD_ID) {
|
||||
this.connections.handleVertexAddDragStart(selectedTarget);
|
||||
event.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const isTargetMoveableElement =
|
||||
this.moveable!.isMoveableElement(selectedTarget) ||
|
||||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
|
||||
|
||||
@@ -15,6 +15,8 @@ type Props = {
|
||||
export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl';
|
||||
export const CONNECTION_ANCHOR_ALT = 'connection anchor';
|
||||
export const CONNECTION_ANCHOR_HIGHLIGHT_OFFSET = 8;
|
||||
export const CONNECTION_VERTEX_ID = 'vertex';
|
||||
export const CONNECTION_VERTEX_ADD_ID = 'vertexAdd';
|
||||
|
||||
const ANCHOR_PADDING = 3;
|
||||
|
||||
|
||||
@@ -6,19 +6,38 @@ import { useStyles2 } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { ConnectionCoordinates } from '../../panelcfg.gen';
|
||||
import { ConnectionState } from '../../types';
|
||||
import { calculateCoordinates, getConnectionStyles, getParentBoundingClientRect } from '../../utils';
|
||||
import {
|
||||
calculateAbsoluteCoords,
|
||||
calculateCoordinates,
|
||||
calculateMidpoint,
|
||||
getConnectionStyles,
|
||||
getParentBoundingClientRect,
|
||||
} from '../../utils';
|
||||
|
||||
import { CONNECTION_VERTEX_ADD_ID, CONNECTION_VERTEX_ID } from './ConnectionAnchors';
|
||||
|
||||
type Props = {
|
||||
setSVGRef: (anchorElement: SVGSVGElement) => void;
|
||||
setLineRef: (anchorElement: SVGLineElement) => void;
|
||||
setSVGVertexRef: (anchorElement: SVGSVGElement) => void;
|
||||
setVertexPathRef: (anchorElement: SVGPathElement) => void;
|
||||
setVertexRef: (anchorElement: SVGCircleElement) => void;
|
||||
scene: Scene;
|
||||
};
|
||||
|
||||
let idCounter = 0;
|
||||
const htmlElementTypes = ['input', 'textarea'];
|
||||
|
||||
export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
export const ConnectionSVG = ({
|
||||
setSVGRef,
|
||||
setLineRef,
|
||||
setSVGVertexRef,
|
||||
setVertexPathRef,
|
||||
setVertexRef,
|
||||
scene,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const headId = Date.now() + '_' + idCounter++;
|
||||
@@ -26,6 +45,7 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
const EDITOR_HEAD_ID = useMemo(() => `editorHead-${headId}`, [headId]);
|
||||
const defaultArrowColor = config.theme2.colors.text.primary;
|
||||
const defaultArrowSize = 2;
|
||||
const maximumVertices = 10;
|
||||
|
||||
const [selectedConnection, setSelectedConnection] = useState<ConnectionState | undefined>(undefined);
|
||||
|
||||
@@ -101,7 +121,7 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
// Figure out target and then target's relative coordinates drawing (if no target do parent)
|
||||
const renderConnections = () => {
|
||||
return scene.connections.state.map((v, idx) => {
|
||||
const { source, target, info } = v;
|
||||
const { source, target, info, vertices } = v;
|
||||
const sourceRect = source.div?.getBoundingClientRect();
|
||||
const parent = source.div?.parentElement;
|
||||
const transformScale = scene.scale;
|
||||
@@ -112,6 +132,7 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
}
|
||||
|
||||
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);
|
||||
const midpoint = calculateMidpoint(x1, y1, x2, y2);
|
||||
|
||||
const { strokeColor, strokeWidth } = getConnectionStyles(info, scene, defaultArrowSize);
|
||||
|
||||
@@ -122,6 +143,30 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
|
||||
const CONNECTION_HEAD_ID = `connectionHead-${headId + Math.random()}`;
|
||||
|
||||
// Create vertex path and populate array of add vertex controls
|
||||
const addVertices: ConnectionCoordinates[] = [];
|
||||
let pathString = `M${x1} ${y1} `;
|
||||
if (vertices?.length) {
|
||||
vertices.map((vertex, index) => {
|
||||
const x = vertex.x;
|
||||
const y = vertex.y;
|
||||
pathString += `L${x * (x2 - x1) + x1} ${y * (y2 - y1) + y1} `;
|
||||
if (index === 0) {
|
||||
// For first vertex
|
||||
addVertices.push(calculateMidpoint(0, 0, x, y));
|
||||
} else {
|
||||
// For all other vertices
|
||||
const previousVertex = vertices[index - 1];
|
||||
addVertices.push(calculateMidpoint(previousVertex.x, previousVertex.y, x, y));
|
||||
}
|
||||
if (index === vertices.length - 1) {
|
||||
// For last vertex
|
||||
addVertices.push(calculateMidpoint(1, 1, x, y));
|
||||
}
|
||||
});
|
||||
pathString += `L${x2} ${y2}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg className={styles.connection} key={idx}>
|
||||
<g onClick={() => selectConnection(v)}>
|
||||
@@ -138,30 +183,106 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill={strokeColor} />
|
||||
</marker>
|
||||
</defs>
|
||||
<line
|
||||
id={`${CONNECTION_LINE_ID}_transparent`}
|
||||
cursor={connectionCursorStyle}
|
||||
pointerEvents="auto"
|
||||
stroke="transparent"
|
||||
strokeWidth={15}
|
||||
style={isSelected ? selectedStyles : {}}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
/>
|
||||
<line
|
||||
id={CONNECTION_LINE_ID}
|
||||
stroke={strokeColor}
|
||||
pointerEvents="auto"
|
||||
strokeWidth={strokeWidth}
|
||||
markerEnd={`url(#${CONNECTION_HEAD_ID})`}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
cursor={connectionCursorStyle}
|
||||
/>
|
||||
{vertices?.length ? (
|
||||
<g>
|
||||
<path
|
||||
id={`${CONNECTION_LINE_ID}_transparent`}
|
||||
d={pathString}
|
||||
cursor={connectionCursorStyle}
|
||||
pointerEvents="auto"
|
||||
stroke="transparent"
|
||||
strokeWidth={15}
|
||||
fill={'none'}
|
||||
style={isSelected ? selectedStyles : {}}
|
||||
/>
|
||||
<path
|
||||
d={pathString}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
fill={'none'}
|
||||
markerEnd={`url(#${CONNECTION_HEAD_ID})`}
|
||||
/>
|
||||
{isSelected && (
|
||||
<g>
|
||||
{vertices.map((value, index) => {
|
||||
const { x, y } = calculateAbsoluteCoords(x1, y1, x2, y2, value.x, value.y);
|
||||
return (
|
||||
<circle
|
||||
id={CONNECTION_VERTEX_ID}
|
||||
data-index={index}
|
||||
key={`${CONNECTION_VERTEX_ID}${index}_${idx}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={5}
|
||||
stroke={strokeColor}
|
||||
className={styles.vertex}
|
||||
cursor={'crosshair'}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{vertices.length < maximumVertices &&
|
||||
addVertices.map((value, index) => {
|
||||
const { x, y } = calculateAbsoluteCoords(x1, y1, x2, y2, value.x, value.y);
|
||||
return (
|
||||
<circle
|
||||
id={CONNECTION_VERTEX_ADD_ID}
|
||||
data-index={index}
|
||||
key={`${CONNECTION_VERTEX_ADD_ID}${index}_${idx}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
stroke={strokeColor}
|
||||
className={styles.addVertex}
|
||||
cursor={'crosshair'}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
) : (
|
||||
<g>
|
||||
<line
|
||||
id={`${CONNECTION_LINE_ID}_transparent`}
|
||||
cursor={connectionCursorStyle}
|
||||
pointerEvents="auto"
|
||||
stroke="transparent"
|
||||
strokeWidth={15}
|
||||
style={isSelected ? selectedStyles : {}}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
/>
|
||||
<line
|
||||
id={CONNECTION_LINE_ID}
|
||||
stroke={strokeColor}
|
||||
pointerEvents="auto"
|
||||
strokeWidth={strokeWidth}
|
||||
markerEnd={`url(#${CONNECTION_HEAD_ID})`}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
cursor={connectionCursorStyle}
|
||||
/>
|
||||
{isSelected && (
|
||||
<circle
|
||||
id={CONNECTION_VERTEX_ADD_ID}
|
||||
data-index={0}
|
||||
cx={midpoint.x}
|
||||
cy={midpoint.y}
|
||||
r={4}
|
||||
stroke={strokeColor}
|
||||
className={styles.addVertex}
|
||||
cursor={'crosshair'}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -186,6 +307,16 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
||||
</defs>
|
||||
<line ref={setLineRef} stroke={defaultArrowColor} strokeWidth={2} markerEnd={`url(#${EDITOR_HEAD_ID})`} />
|
||||
</svg>
|
||||
<svg ref={setSVGVertexRef} className={styles.editorSVG}>
|
||||
<path
|
||||
ref={setVertexPathRef}
|
||||
stroke={defaultArrowColor}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={'5, 5'}
|
||||
fill={'none'}
|
||||
/>
|
||||
<circle ref={setVertexRef} stroke={defaultArrowColor} r={4} className={styles.vertex} />
|
||||
</svg>
|
||||
{renderConnections()}
|
||||
</>
|
||||
);
|
||||
@@ -207,4 +338,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
vertex: css({
|
||||
fill: '#44aaff',
|
||||
strokeWidth: 2,
|
||||
}),
|
||||
addVertex: css({
|
||||
fill: '#44aaff',
|
||||
opacity: 0.5,
|
||||
strokeWidth: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,12 +2,18 @@ import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CanvasConnection, ConnectionPath } from 'app/features/canvas';
|
||||
import { CanvasConnection, ConnectionCoordinates, ConnectionPath } from 'app/features/canvas';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { ConnectionState } from '../../types';
|
||||
import { getConnections, getParentBoundingClientRect, isConnectionSource, isConnectionTarget } from '../../utils';
|
||||
import {
|
||||
calculateCoordinates,
|
||||
getConnections,
|
||||
getParentBoundingClientRect,
|
||||
isConnectionSource,
|
||||
isConnectionTarget,
|
||||
} from '../../utils';
|
||||
|
||||
import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors';
|
||||
import { ConnectionSVG } from './ConnectionSVG';
|
||||
@@ -17,9 +23,13 @@ export class Connections {
|
||||
connectionAnchorDiv?: HTMLDivElement;
|
||||
connectionSVG?: SVGElement;
|
||||
connectionLine?: SVGLineElement;
|
||||
connectionSVGVertex?: SVGElement;
|
||||
connectionVertexPath?: SVGPathElement;
|
||||
connectionVertex?: SVGCircleElement;
|
||||
connectionSource?: ElementState;
|
||||
connectionTarget?: ElementState;
|
||||
isDrawingConnection?: boolean;
|
||||
selectedVertexIndex?: number;
|
||||
didConnectionLeaveHighlight?: boolean;
|
||||
state: ConnectionState[] = [];
|
||||
readonly selection = new BehaviorSubject<ConnectionState | undefined>(undefined);
|
||||
@@ -62,6 +72,18 @@ export class Connections {
|
||||
this.connectionLine = connectionLine;
|
||||
};
|
||||
|
||||
setConnectionSVGVertexRef = (connectionSVG: SVGSVGElement) => {
|
||||
this.connectionSVGVertex = connectionSVG;
|
||||
};
|
||||
|
||||
setConnectionVertexRef = (connectionVertex: SVGCircleElement) => {
|
||||
this.connectionVertex = connectionVertex;
|
||||
};
|
||||
|
||||
setConnectionVertexPathRef = (connectionVertexPath: SVGPathElement) => {
|
||||
this.connectionVertexPath = connectionVertexPath;
|
||||
};
|
||||
|
||||
// Recursively find the first parent that is a canvas element
|
||||
findElementTarget = (element: Element): ElementState | undefined => {
|
||||
let elementTarget = undefined;
|
||||
@@ -251,6 +273,192 @@ export class Connections {
|
||||
}
|
||||
};
|
||||
|
||||
// Handles mousemove and mouseup events when dragging an existing vertex
|
||||
vertexListener = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(this.connectionVertex && this.scene.div && this.scene.div.parentElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformScale = this.scene.scale;
|
||||
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||
|
||||
if (!parentBoundingRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = (event.pageX - parentBoundingRect.x) / transformScale ?? 0;
|
||||
const y = (event.pageY - parentBoundingRect.y) / transformScale ?? 0;
|
||||
|
||||
this.connectionVertex?.setAttribute('cx', `${x}`);
|
||||
this.connectionVertex?.setAttribute('cy', `${y}`);
|
||||
|
||||
const sourceRect = this.selection.value!.source.div!.getBoundingClientRect();
|
||||
|
||||
// calculate relative coordinates based on source and target coorindates of connection
|
||||
const { x1, y1, x2, y2 } = calculateCoordinates(
|
||||
sourceRect,
|
||||
parentBoundingRect,
|
||||
this.selection.value?.info!,
|
||||
this.selection.value!.target,
|
||||
transformScale
|
||||
);
|
||||
|
||||
let vx1 = x1;
|
||||
let vy1 = y1;
|
||||
let vx2 = x2;
|
||||
let vy2 = y2;
|
||||
if (this.selection.value && this.selection.value.vertices) {
|
||||
if (this.selectedVertexIndex !== undefined && this.selectedVertexIndex > 0) {
|
||||
vx1 += this.selection.value.vertices[this.selectedVertexIndex - 1].x * (x2 - x1);
|
||||
vy1 += this.selection.value.vertices[this.selectedVertexIndex - 1].y * (y2 - y1);
|
||||
}
|
||||
if (
|
||||
this.selectedVertexIndex !== undefined &&
|
||||
this.selectedVertexIndex < this.selection.value.vertices.length - 1
|
||||
) {
|
||||
vx2 = this.selection.value.vertices[this.selectedVertexIndex + 1].x * (x2 - x1) + x1;
|
||||
vy2 = this.selection.value.vertices[this.selectedVertexIndex + 1].y * (y2 - y1) + y1;
|
||||
}
|
||||
}
|
||||
|
||||
// Display temporary vertex during drag
|
||||
this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${x} ${y} L${vx2} ${vy2}`);
|
||||
this.connectionSVGVertex!.style.display = 'block';
|
||||
|
||||
// Handle mouseup
|
||||
if (!event.buttons) {
|
||||
// Remove existing event listener
|
||||
this.scene.selecto?.rootContainer?.removeEventListener('mousemove', this.vertexListener);
|
||||
this.scene.selecto?.rootContainer?.removeEventListener('mouseup', this.vertexListener);
|
||||
this.scene.selecto!.rootContainer!.style.cursor = 'auto';
|
||||
this.connectionSVGVertex!.style.display = 'none';
|
||||
|
||||
// call onChange here and update appropriate index of connection vertices array
|
||||
const connectionIndex = this.selection.value?.index;
|
||||
const vertexIndex = this.selectedVertexIndex;
|
||||
|
||||
if (connectionIndex !== undefined && vertexIndex !== undefined) {
|
||||
const currentSource = this.scene.connections.state[connectionIndex].source;
|
||||
if (currentSource.options.connections) {
|
||||
const currentConnections = [...currentSource.options.connections];
|
||||
if (currentConnections[connectionIndex].vertices) {
|
||||
const currentVertices = [...currentConnections[connectionIndex].vertices!];
|
||||
const currentVertex = { ...currentVertices[vertexIndex] };
|
||||
|
||||
currentVertex.x = (x - x1) / (x2 - x1);
|
||||
currentVertex.y = (y - y1) / (y2 - y1);
|
||||
|
||||
currentVertices[vertexIndex] = currentVertex;
|
||||
currentConnections[connectionIndex] = {
|
||||
...currentConnections[connectionIndex],
|
||||
vertices: currentVertices,
|
||||
};
|
||||
|
||||
// Update save model
|
||||
currentSource.onChange({ ...currentSource.options, connections: currentConnections });
|
||||
this.updateState();
|
||||
this.scene.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handles mousemove and mouseup events when dragging a new vertex
|
||||
vertexAddListener = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(this.connectionVertex && this.scene.div && this.scene.div.parentElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformScale = this.scene.scale;
|
||||
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||
|
||||
if (!parentBoundingRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = (event.pageX - parentBoundingRect.x) / transformScale ?? 0;
|
||||
const y = (event.pageY - parentBoundingRect.y) / transformScale ?? 0;
|
||||
|
||||
this.connectionVertex?.setAttribute('cx', `${x}`);
|
||||
this.connectionVertex?.setAttribute('cy', `${y}`);
|
||||
|
||||
const sourceRect = this.selection.value!.source.div!.getBoundingClientRect();
|
||||
|
||||
// calculate relative coordinates based on source and target coorindates of connection
|
||||
const { x1, y1, x2, y2 } = calculateCoordinates(
|
||||
sourceRect,
|
||||
parentBoundingRect,
|
||||
this.selection.value?.info!,
|
||||
this.selection.value!.target,
|
||||
transformScale
|
||||
);
|
||||
|
||||
let vx1 = x1;
|
||||
let vy1 = y1;
|
||||
let vx2 = x2;
|
||||
let vy2 = y2;
|
||||
if (this.selection.value && this.selection.value.vertices) {
|
||||
if (this.selectedVertexIndex !== undefined && this.selectedVertexIndex > 0) {
|
||||
vx1 += this.selection.value.vertices[this.selectedVertexIndex - 1].x * (x2 - x1);
|
||||
vy1 += this.selection.value.vertices[this.selectedVertexIndex - 1].y * (y2 - y1);
|
||||
}
|
||||
if (this.selectedVertexIndex !== undefined && this.selectedVertexIndex < this.selection.value.vertices.length) {
|
||||
vx2 = this.selection.value.vertices[this.selectedVertexIndex].x * (x2 - x1) + x1;
|
||||
vy2 = this.selection.value.vertices[this.selectedVertexIndex].y * (y2 - y1) + y1;
|
||||
}
|
||||
}
|
||||
|
||||
// Display temporary vertex during drag
|
||||
this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${x} ${y} L${vx2} ${vy2}`);
|
||||
this.connectionSVGVertex!.style.display = 'block';
|
||||
|
||||
// Handle mouseup
|
||||
if (!event.buttons) {
|
||||
// Remove existing event listener
|
||||
this.scene.selecto?.rootContainer?.removeEventListener('mousemove', this.vertexAddListener);
|
||||
this.scene.selecto?.rootContainer?.removeEventListener('mouseup', this.vertexAddListener);
|
||||
this.scene.selecto!.rootContainer!.style.cursor = 'auto';
|
||||
this.connectionSVGVertex!.style.display = 'none';
|
||||
|
||||
// call onChange here and insert new vertex at appropriate index of connection vertices array
|
||||
const connectionIndex = this.selection.value?.index;
|
||||
const vertexIndex = this.selectedVertexIndex;
|
||||
|
||||
if (connectionIndex !== undefined && vertexIndex !== undefined) {
|
||||
const currentSource = this.scene.connections.state[connectionIndex].source;
|
||||
if (currentSource.options.connections) {
|
||||
const currentConnections = [...currentSource.options.connections];
|
||||
const newVertex = { x: (x - x1) / (x2 - x1), y: (y - y1) / (y2 - y1) };
|
||||
if (currentConnections[connectionIndex].vertices) {
|
||||
const currentVertices = [...currentConnections[connectionIndex].vertices!];
|
||||
currentVertices.splice(vertexIndex, 0, newVertex);
|
||||
currentConnections[connectionIndex] = {
|
||||
...currentConnections[connectionIndex],
|
||||
vertices: currentVertices,
|
||||
};
|
||||
} else {
|
||||
// For first vertex creation
|
||||
const currentVertices: ConnectionCoordinates[] = [newVertex];
|
||||
currentConnections[connectionIndex] = {
|
||||
...currentConnections[connectionIndex],
|
||||
vertices: currentVertices,
|
||||
};
|
||||
}
|
||||
|
||||
// Update save model
|
||||
currentSource.onChange({ ...currentSource.options, connections: currentConnections });
|
||||
this.updateState();
|
||||
this.scene.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleConnectionDragStart = (selectedTarget: HTMLElement, clientX: number, clientY: number) => {
|
||||
this.scene.selecto!.rootContainer!.style.cursor = 'crosshair';
|
||||
if (this.connectionSVG && this.connectionLine && this.scene.div && this.scene.div.parentElement) {
|
||||
@@ -283,6 +491,26 @@ export class Connections {
|
||||
this.scene.selecto?.rootContainer?.addEventListener('mousemove', this.connectionListener);
|
||||
};
|
||||
|
||||
// Add event listener at root container during existing vertex drag
|
||||
handleVertexDragStart = (selectedTarget: HTMLElement) => {
|
||||
// Get vertex index from selected target data
|
||||
this.selectedVertexIndex = Number(selectedTarget.getAttribute('data-index'));
|
||||
this.scene.selecto!.rootContainer!.style.cursor = 'crosshair';
|
||||
|
||||
this.scene.selecto?.rootContainer?.addEventListener('mousemove', this.vertexListener);
|
||||
this.scene.selecto?.rootContainer?.addEventListener('mouseup', this.vertexListener);
|
||||
};
|
||||
|
||||
// Add event listener at root container during creation of new vertex
|
||||
handleVertexAddDragStart = (selectedTarget: HTMLElement) => {
|
||||
// Get vertex index from selected target data
|
||||
this.selectedVertexIndex = Number(selectedTarget.getAttribute('data-index'));
|
||||
this.scene.selecto!.rootContainer!.style.cursor = 'crosshair';
|
||||
|
||||
this.scene.selecto?.rootContainer?.addEventListener('mousemove', this.vertexAddListener);
|
||||
this.scene.selecto?.rootContainer?.addEventListener('mouseup', this.vertexAddListener);
|
||||
};
|
||||
|
||||
onChange = (current: ConnectionState, update: CanvasConnection) => {
|
||||
const connections = current.source.options.connections?.splice(0) ?? [];
|
||||
connections[current.index] = update;
|
||||
@@ -299,7 +527,14 @@ export class Connections {
|
||||
return (
|
||||
<>
|
||||
<ConnectionAnchors setRef={this.setConnectionAnchorRef} handleMouseLeave={this.handleMouseLeave} />
|
||||
<ConnectionSVG setSVGRef={this.setConnectionSVGRef} setLineRef={this.setConnectionLineRef} scene={this.scene} />
|
||||
<ConnectionSVG
|
||||
setSVGRef={this.setConnectionSVGRef}
|
||||
setLineRef={this.setConnectionLineRef}
|
||||
setSVGVertexRef={this.setConnectionSVGVertexRef}
|
||||
setVertexPathRef={this.setConnectionVertexPathRef}
|
||||
setVertexRef={this.setConnectionVertexRef}
|
||||
scene={this.scene}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ composableKinds: PanelCfg: {
|
||||
path: ConnectionPath
|
||||
color?: ui.ColorDimensionConfig
|
||||
size?: ui.ScaleDimensionConfig
|
||||
vertices?: [...ConnectionCoordinates]
|
||||
} @cuetsy(kind="interface")
|
||||
CanvasElementOptions: {
|
||||
name: string
|
||||
|
||||
@@ -81,8 +81,13 @@ export interface CanvasConnection {
|
||||
source: ConnectionCoordinates;
|
||||
target: ConnectionCoordinates;
|
||||
targetName?: string;
|
||||
vertices?: Array<ConnectionCoordinates>;
|
||||
}
|
||||
|
||||
export const defaultCanvasConnection: Partial<CanvasConnection> = {
|
||||
vertices: [],
|
||||
};
|
||||
|
||||
export interface CanvasElementOptions {
|
||||
background?: BackgroundConfig;
|
||||
border?: LineConfig;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CanvasConnection } from '../../../features/canvas';
|
||||
import { ElementState } from '../../../features/canvas/runtime/element';
|
||||
|
||||
import { ConnectionCoordinates } from './panelcfg.gen';
|
||||
|
||||
export enum LayerActionID {
|
||||
Delete = 'delete',
|
||||
Duplicate = 'duplicate',
|
||||
@@ -39,4 +41,5 @@ export interface ConnectionState {
|
||||
source: ElementState;
|
||||
target: ElementState;
|
||||
info: CanvasConnection;
|
||||
vertices?: ConnectionCoordinates[];
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ export function getConnections(sceneByName: Map<string, ElementState>) {
|
||||
source: v,
|
||||
target,
|
||||
info: c,
|
||||
vertices: c.vertices ?? undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -347,6 +348,21 @@ export const calculateCoordinates = (
|
||||
return { x1, y1, x2, y2 };
|
||||
};
|
||||
|
||||
export const calculateMidpoint = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
return { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
|
||||
};
|
||||
|
||||
export const calculateAbsoluteCoords = (
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
valueX: number,
|
||||
valueY: number
|
||||
) => {
|
||||
return { x: valueX * (x2 - x1) + x1, y: valueY * (y2 - y1) + y1 };
|
||||
};
|
||||
|
||||
// @TODO revisit, currently returning last row index for field
|
||||
export const getRowIndex = (fieldName: string | undefined, scene: Scene) => {
|
||||
if (fieldName) {
|
||||
|
||||
Reference in New Issue
Block a user