diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 526f2a245a0..db8a1d05c0c 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -28,12 +28,12 @@ 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, + Connections, 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'; +} 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'; diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx index 879a1a44cff..cbd13b41e24 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx @@ -15,8 +15,6 @@ 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; diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx index 03fd8875ea2..c8f897b49bc 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx @@ -16,7 +16,7 @@ import { getParentBoundingClientRect, } from '../../utils'; -import { CONNECTION_VERTEX_ADD_ID, CONNECTION_VERTEX_ID } from './ConnectionAnchors'; +import { CONNECTION_VERTEX_ADD_ID, CONNECTION_VERTEX_ID } from './Connections'; type Props = { setSVGRef: (anchorElement: SVGSVGElement) => void; diff --git a/public/app/plugins/panel/canvas/components/connections/Connections.tsx b/public/app/plugins/panel/canvas/components/connections/Connections.tsx index 25eb19644f1..2c1c4943ec2 100644 --- a/public/app/plugins/panel/canvas/components/connections/Connections.tsx +++ b/public/app/plugins/panel/canvas/components/connections/Connections.tsx @@ -8,6 +8,7 @@ import { Scene } from 'app/features/canvas/runtime/scene'; import { ConnectionState } from '../../types'; import { + calculateAngle, calculateCoordinates, getConnections, getParentBoundingClientRect, @@ -18,6 +19,11 @@ import { import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors'; 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 + export class Connections { scene: Scene; connectionAnchorDiv?: HTMLDivElement; @@ -275,6 +281,8 @@ export class Connections { // Handles mousemove and mouseup events when dragging an existing vertex vertexListener = (event: MouseEvent) => { + this.scene.selecto!.rootContainer!.style.cursor = 'crosshair'; + event.preventDefault(); if (!(this.connectionVertex && this.scene.div && this.scene.div.parentElement)) { @@ -323,9 +331,53 @@ export class Connections { } } - // Display temporary vertex during drag - this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${x} ${y} L${vx2} ${vy2}`); - this.connectionSVGVertex!.style.display = 'block'; + // Check if slope before vertex and after vertex is within snapping tolerance + let xSnap = x; + let ySnap = y; + let deleteVertex = false; + // Ignore if control key being held + if (!event.ctrlKey) { + // Check if segment before and after vertex are close to vertical or horizontal + const verticalBefore = Math.abs((x - vx1) / (y - vy1)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const verticalAfter = Math.abs((x - vx2) / (y - vy2)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const horizontalBefore = Math.abs((y - vy1) / (x - vx1)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const horizontalAfter = Math.abs((y - vy2) / (x - vx2)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + + if (verticalBefore) { + xSnap = vx1; + } else if (verticalAfter) { + xSnap = vx2; + } + if (horizontalBefore) { + ySnap = vy1; + } else if (horizontalAfter) { + ySnap = vy2; + } + + if ((verticalBefore || verticalAfter) && (horizontalBefore || horizontalAfter)) { + this.scene.selecto!.rootContainer!.style.cursor = 'move'; + } else if (verticalBefore || verticalAfter) { + this.scene.selecto!.rootContainer!.style.cursor = 'col-resize'; + } else if (horizontalBefore || horizontalAfter) { + this.scene.selecto!.rootContainer!.style.cursor = 'row-resize'; + } + + const angleOverall = calculateAngle(vx1, vy1, vx2, vy2); + const angleBefore = calculateAngle(vx1, vy1, x, y); + deleteVertex = Math.abs(angleBefore - angleOverall) < CONNECTION_VERTEX_SNAP_TOLERANCE; + } + + if (deleteVertex) { + // Display temporary vertex removal + this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${vx2} ${vy2}`); + this.connectionSVGVertex!.style.display = 'block'; + this.connectionVertex.style.display = 'none'; + } else { + // Display temporary vertex during drag + this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${xSnap} ${ySnap} L${vx2} ${vy2}`); + this.connectionSVGVertex!.style.display = 'block'; + this.connectionVertex.style.display = 'block'; + } // Handle mouseup if (!event.buttons) { @@ -345,12 +397,18 @@ export class 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); + if (deleteVertex) { + currentVertices.splice(vertexIndex, 1); + } else { + const currentVertex = { ...currentVertices[vertexIndex] }; + + currentVertex.x = (xSnap - x1) / (x2 - x1); + currentVertex.y = (ySnap - y1) / (y2 - y1); + + currentVertices[vertexIndex] = currentVertex; + } - currentVertices[vertexIndex] = currentVertex; currentConnections[connectionIndex] = { ...currentConnections[connectionIndex], vertices: currentVertices, @@ -368,6 +426,8 @@ export class Connections { // Handles mousemove and mouseup events when dragging a new vertex vertexAddListener = (event: MouseEvent) => { + this.scene.selecto!.rootContainer!.style.cursor = 'crosshair'; + event.preventDefault(); if (!(this.connectionVertex && this.scene.div && this.scene.div.parentElement)) { @@ -413,9 +473,40 @@ export class Connections { } } - // Display temporary vertex during drag - this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${x} ${y} L${vx2} ${vy2}`); + // Check if slope before vertex and after vertex is within snapping tolerance + let xSnap = x; + let ySnap = y; + // Ignore if control key being held + if (!event.ctrlKey) { + // Check if segment before and after vertex are close to vertical or horizontal + const verticalBefore = Math.abs((x - vx1) / (y - vy1)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const verticalAfter = Math.abs((x - vx2) / (y - vy2)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const horizontalBefore = Math.abs((y - vy1) / (x - vx1)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + const horizontalAfter = Math.abs((y - vy2) / (x - vx2)) < CONNECTION_VERTEX_ORTHO_TOLERANCE; + + if (verticalBefore) { + xSnap = vx1; + } else if (verticalAfter) { + xSnap = vx2; + } + if (horizontalBefore) { + ySnap = vy1; + } else if (horizontalAfter) { + ySnap = vy2; + } + + if ((verticalBefore || verticalAfter) && (horizontalBefore || horizontalAfter)) { + this.scene.selecto!.rootContainer!.style.cursor = 'move'; + } else if (verticalBefore || verticalAfter) { + this.scene.selecto!.rootContainer!.style.cursor = 'col-resize'; + } else if (horizontalBefore || horizontalAfter) { + this.scene.selecto!.rootContainer!.style.cursor = 'row-resize'; + } + } + + this.connectionVertexPath?.setAttribute('d', `M${vx1} ${vy1} L${xSnap} ${ySnap} L${vx2} ${vy2}`); this.connectionSVGVertex!.style.display = 'block'; + this.connectionVertex.style.display = 'block'; // Handle mouseup if (!event.buttons) { @@ -495,7 +586,6 @@ export class Connections { 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); @@ -505,7 +595,6 @@ export class Connections { 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); diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts index f9a4b3f9a73..ee78835dd1c 100644 --- a/public/app/plugins/panel/canvas/utils.ts +++ b/public/app/plugins/panel/canvas/utils.ts @@ -363,6 +363,10 @@ export const calculateAbsoluteCoords = ( return { x: valueX * (x2 - x1) + x1, y: valueY * (y2 - y1) + y1 }; }; +export const calculateAngle = (x1: number, y1: number, x2: number, y2: number) => { + return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI; +}; + // @TODO revisit, currently returning last row index for field export const getRowIndex = (fieldName: string | undefined, scene: Scene) => { if (fieldName) {