Skip to content

Commit

Permalink
[956] Implement bordered snap to parent container
Browse files Browse the repository at this point in the history
Bug: #956
Signed-off-by: Laurent Fasani <laurent.fasani@obeo.fr>
  • Loading branch information
lfasani committed Feb 9, 2022
1 parent a225054 commit 756f163
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 8 deletions.
6 changes: 4 additions & 2 deletions frontend/src/diagram/sprotty/DependencyInjection.ts
Expand Up @@ -10,7 +10,7 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { Node } from 'diagram/sprotty/Diagram.types';
import { BorderNode, Node } from 'diagram/sprotty/Diagram.types';
import { DiagramServer, HIDE_CONTEXTUAL_TOOLBAR_ACTION, SPROTTY_DELETE_ACTION } from 'diagram/sprotty/DiagramServer';
import { SetActiveConnectorToolsAction, SetActiveToolAction } from 'diagram/sprotty/DiagramServer.types';
import { edgeCreationFeedback } from 'diagram/sprotty/edgeCreationFeedback';
Expand Down Expand Up @@ -123,7 +123,9 @@ const siriusWebContainerModule = new ContainerModule((bind, unbind, isBound, reb
// @ts-ignore
configureModelElement(context, 'node:list:item', Node, ListItemView);
// @ts-ignore
configureView({ bind, isBound }, 'port:square', RectangleView);
configureModelElement(context, 'port:rectangle', BorderNode, RectangleView);
// @ts-ignore
configureModelElement(context, 'port:image', BorderNode, ImageView);
configureView({ bind, isBound }, 'edge:straight', EdgeView);
// @ts-ignore
configureModelElement(context, 'label:inside-center', SEditableLabel, LabelView);
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/diagram/sprotty/Diagram.types.ts
Expand Up @@ -30,6 +30,16 @@ export class Node extends SNode implements WithEditableLabel {
targetObjectLabel: string;
}

export class BorderNode extends SPort implements WithEditableLabel {
editableLabel?: EditableLabel & Label;
descriptionId: string;
kind: string;
style: INodeStyle;
targetObjectId: string;
targetObjectKind: string;
targetObjectLabel: string;
}

export interface INodeStyle {}

export class ImageNodeStyle implements INodeStyle {
Expand Down Expand Up @@ -108,5 +118,3 @@ export class LabelStyle {
strikeThrough: boolean;
underline: boolean;
}

export class Port extends SPort {}
28 changes: 26 additions & 2 deletions frontend/src/diagram/sprotty/convertDiagram.tsx
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019, 2021 Obeo.
* Copyright (c) 2019, 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -25,6 +25,7 @@ import {
} from 'diagram/DiagramWebSocketContainer.types';
import {
ArrowStyle,
BorderNode,
Diagram,
Edge,
EdgeStyle,
Expand Down Expand Up @@ -111,7 +112,7 @@ const convertNode = (gqlNode: GQLNode, httpOrigin: string, readOnly: boolean, au

const convertedLabel = convertLabel(label, httpOrigin, readOnly);
const convertedBorderNodes = (borderNodes ?? []).map((borderNode) =>
convertNode(borderNode, httpOrigin, readOnly, autoLayout)
convertBorderNode(borderNode, httpOrigin, readOnly, autoLayout)
);
const convertedChildNodes = (childNodes ?? []).map((childNode) =>
convertNode(childNode, httpOrigin, readOnly, autoLayout)
Expand All @@ -134,6 +135,29 @@ const convertNode = (gqlNode: GQLNode, httpOrigin: string, readOnly: boolean, au

return node;
};
const convertBorderNode = (
gqlNode: GQLNode,
httpOrigin: string,
readOnly: boolean,
autoLayout: boolean
): BorderNode => {
const { id, descriptionId, type, targetObjectId, targetObjectKind, targetObjectLabel, size, position, style } =
gqlNode;

const node: BorderNode = new BorderNode();
node.id = id;
node.type = type.replace('node:', 'port:');
node.kind = `siriusComponents://graphical?representationType=Diagram&type=Node`;
node.descriptionId = descriptionId;
node.style = convertNodeStyle(style, httpOrigin);
node.targetObjectId = targetObjectId;
node.targetObjectKind = targetObjectKind;
node.targetObjectLabel = targetObjectLabel;
node.position = position;
node.size = size;
node.features = handleNodeFeatures(gqlNode, readOnly, autoLayout);
return node;
};

const handleNodeFeatures = (gqlNode: GQLNode, readOnly: boolean, autoLayout: boolean): FeatureSet => {
const features = new Set<symbol>([
Expand Down
66 changes: 64 additions & 2 deletions frontend/src/diagram/sprotty/siriusDragAndDropMouseListener.ts
Expand Up @@ -12,15 +12,20 @@
*******************************************************************************/
import {
Action,
center,
Dimension,
findParentByFeature,
isViewport,
MoveMouseListener,
Point,
SModelElement,
SNode,
SPort,
} from 'sprotty';
import { ElementResize, ResizeAction } from './resize/siriusResize';
import { RectangleSide, snapToRectangle } from './utils/geometry';

const PORT_OFFSET_PERCENTAGE = 25;

/**
* A common listener for drag and drop actions. This class allows to enter in resize or move mode.
Expand Down Expand Up @@ -94,7 +99,9 @@ export class SiriusDragAndDropMouseListener extends MoveMouseListener {
protected snap(position: Point, element: SModelElement, isSnap: boolean): Point {
let newPosition = super.snap(position, element, isSnap);
if (this.isSNode(element)) {
return this.getValidPosition(element, newPosition);
return this.getValidChildPosition(element, newPosition);
} else if (this.isSPort(element)) {
return this.getValidPortPosition(element, newPosition);
}
return newPosition;
}
Expand All @@ -104,7 +111,7 @@ export class SiriusDragAndDropMouseListener extends MoveMouseListener {
* @param element the element currently moved.
* @param position the new candidate position.
*/
private getValidPosition(element: SNode, position: Point): Point {
private getValidChildPosition(element: SNode, position: Point): Point {
const parent = element.parent;
if (this.isSNode(parent)) {
const bottomRight = {
Expand All @@ -124,6 +131,57 @@ export class SiriusDragAndDropMouseListener extends MoveMouseListener {
return position;
}

/**
* Provides the position on the parent bounding box's border.
* @param sPort the sPort currently moved.
* @param position the new candidate position of the SPort upper left corner.
* @returns the real position of the port.
*/
private getValidPortPosition(sPort: SPort, newSportPosition: Point): Point {
const parent = sPort.parent;
if (this.isSNode(parent)) {
// Determine on which side the port should be associated
const translation = {
x: newSportPosition.x - sPort.bounds.x,
y: newSportPosition.y - sPort.bounds.y,
} as Point;
const currentSPortCenter = center(sPort.bounds);
const candidateSPortCenter = { x: currentSPortCenter.x + translation.x, y: currentSPortCenter.y + translation.y };

const { pointOnRectangleSide, side } = snapToRectangle(candidateSPortCenter, {
x: 0, // because newSportPosition has coordinates relative to parent
y: 0,
width: parent.bounds.width,
height: parent.bounds.height,
});

// Move the port according to the offset and the side position
if (side === RectangleSide.north) {
return {
x: pointOnRectangleSide.x - sPort.bounds.width / 2,
y: pointOnRectangleSide.y - (sPort.bounds.height * (100 - PORT_OFFSET_PERCENTAGE)) / 100,
};
} else if (side === RectangleSide.south) {
return {
x: pointOnRectangleSide.x - sPort.bounds.width / 2,
y: pointOnRectangleSide.y - (sPort.bounds.height * PORT_OFFSET_PERCENTAGE) / 100,
};
} else if (side === RectangleSide.west) {
return {
x: pointOnRectangleSide.x - (sPort.bounds.width * (100 - PORT_OFFSET_PERCENTAGE)) / 100,
y: pointOnRectangleSide.y - sPort.bounds.height / 2,
};
} else if (side === RectangleSide.east) {
return {
x: pointOnRectangleSide.x - (sPort.bounds.width * PORT_OFFSET_PERCENTAGE) / 100,
y: pointOnRectangleSide.y - sPort.bounds.height / 2,
};
}
}
// the SPort is not moved
return { x: sPort.bounds.x, y: sPort.bounds.y };
}

protected reset() {
this.intialTarget = undefined;
this.startingPosition = undefined;
Expand All @@ -144,6 +202,10 @@ export class SiriusDragAndDropMouseListener extends MoveMouseListener {
return element instanceof SNode;
}

protected isSPort(element: SModelElement): element is SPort {
return element instanceof SPort;
}

/**
* Computes the potential new position and new size of the element being resized.
* It is only "potential" because the ResizeAction can prevent the resize.
Expand Down
111 changes: 111 additions & 0 deletions frontend/src/diagram/sprotty/utils/geometry.tsx
@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2022 THALES GLOBAL SERVICES.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { Bounds, Point } from 'sprotty';

export enum RectangleSide {
'north',
'south',
'west',
'east',
}
/**
* Returns the distance between the point p and the segment made of p1 and p2
* @param p the point
* @param p1 the first point of the segment
* @param p2 the second point of the segment
*/
export function closestPointOnSegment(p: Point, p1: Point, p2: Point): Point {
var A = p.x - p1.x;
var B = p.y - p1.y;
var C = p2.x - p1.x;
var D = p2.y - p1.y;

var dot = A * C + B * D;
var lengthSquare = C * C + D * D;
// param is the projection distance from p1 on the segment. This is the fraction of the segment that p is closest to.
var param = -1;
if (lengthSquare != 0)
//if the line has 0 length
param = dot / lengthSquare;

var nearestPoint: Point = p1;

if (param < 0) {
// The point is not "in front of" the segment. It is "away" on the p1 side
nearestPoint = { x: p1.x, y: p1.y };
} else if (param > 1) {
// The point is not "in front of" the segment. It is "away" on the p1 side
nearestPoint = { x: p2.x, y: p2.y };
} else {
// The point is "in front of" the segment
// get the projection of p on the segment
nearestPoint = { x: p1.x + param * C, y: p1.y + param * D };
}

return nearestPoint;
}

export interface SnapToRectangleInfo {
pointOnRectangleSide: Point;
side: RectangleSide;
}

/**
* Returns a point on the rectangle that is the closest from the given p point.
*/
export const snapToRectangle = (point: Point, rectangle: Bounds): SnapToRectangleInfo => {
const upperLeftCorner: Point = { x: rectangle.x, y: rectangle.y };
const upperRightCorner: Point = { x: rectangle.x + rectangle.width, y: rectangle.y };
const bottomLeftCorner: Point = { x: rectangle.x, y: rectangle.y + rectangle.height };
const bottomRightCorner: Point = { x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height };

let closestPointInfo = computeClosestPoint(null, point, upperLeftCorner, upperRightCorner, RectangleSide.north);
closestPointInfo = computeClosestPoint(
closestPointInfo,
point,
upperRightCorner,
bottomRightCorner,
RectangleSide.east
);
closestPointInfo = computeClosestPoint(
closestPointInfo,
point,
bottomRightCorner,
bottomLeftCorner,
RectangleSide.south
);
closestPointInfo = computeClosestPoint(
closestPointInfo,
point,
bottomLeftCorner,
upperLeftCorner,
RectangleSide.west
);

return { pointOnRectangleSide: closestPointInfo.pointOnSegment, side: closestPointInfo.side };
};

function computeClosestPoint(current, p: Point, p1: Point, p2: Point, side: RectangleSide) {
const pointOnSegment: Point = closestPointOnSegment(p, p1, p2);
let distanceSquare = getDistanceSquare(p, pointOnSegment);
if (current != null && distanceSquare > current.distanceSquare) {
return { pointOnSegment: current.pointOnSegment, distanceSquare: current.distanceSquare, side: current.side };
}
return { pointOnSegment, distanceSquare, side };
}

function getDistanceSquare(p1: Point, p2: Point): number {
var dx = p1.x - p2.x;
var dy = p1.y - p2.y;
return dx * dx + dy * dy;
}

0 comments on commit 756f163

Please sign in to comment.