Canvas: Add snapping to vertex edit (#84417)

* 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

* Canvas: Add snapping to vertex edit

* Remove unused color styling

* Add horizontal and vertical snap for vertex edit

* Add snapping during vertex creation

* Hide vertex before removal
This commit is contained in:
Drew Slobodnjak 2024-03-20 23:49:28 -07:00 committed by GitHub
parent f96de9d0f8
commit 3877d976d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 108 additions and 17 deletions

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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) {