Skip to content

Commit

Permalink
[956] Implement bordered snap to parent container
Browse files Browse the repository at this point in the history
The border node enters its parent container with 8px.

Bug: #956
Signed-off-by: Laurent Fasani <laurent.fasani@obeo.fr>
  • Loading branch information
lfasani committed Feb 25, 2022
1 parent 6e1bd37 commit 33a1d0d
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Expand Up @@ -9,6 +9,7 @@
- [ADR-041] Add the ability to contribute additional services to the EMFQueryService
- [ADR-042] Use wildcard collections instead of List<Object> in Providers
- [ADR-043] Consider multiple objects as the input of a form
- [ADR-044] Use border node "snap to parent container" algorithm

=== Deprecation warning
- [core] The various DTOs related to the creation and renaming of both documents and representations will be removed from the project at some point. The only reason why we will keep them for the moment is that some of them are used to trigger some specific behavior in the `EditingContextEventProcessor`. The split of the representation metadata should help us remove those special use cases
Expand Down Expand Up @@ -52,6 +53,7 @@
- https://github.com/eclipse-sirius/sirius-components/issues/1054[#1054] [diagram] Add missing variables to compute the label of an edge
- https://github.com/eclipse-sirius/sirius-components/issues/1063[#1063] [explorer] It is now possible to expand or collapse items in the explorer without selecting them by clicking directly on the expand/collapse arrow icon
- https://github.com/eclipse-sirius/sirius-components/issues/1068[#1068] [form] Add support for displaying details on arbitrary element kinds
- https://github.com/eclipse-sirius/sirius-components/issues/1049[#1049] [diagram] Add border node enhancements

=== New features

Expand Down
30 changes: 30 additions & 0 deletions doc/adrs/044_add_border_node_snap_to_container_algorithm.adoc
@@ -0,0 +1,30 @@
= ADR-44 - Use border node "snap to parent container" algorithm

== Context

On the front end, consider a diagram containing nodes with border nodes.

The user has the abilty to move the border nodes on a side of the parent rectangle node or onto another side.

Until now, the border node is considered as any node and consequently the user has the ability to move the border node inside the parent node and only inside.

== Decision

A "snap to parent rectangle" is added.
It is used by the front end code.

Now, when the user moves the border node, it is moved only on the border of the parent node, outside the parent node.

An 8px offset is used to have the border node entering the parent node.

=== Algorithm details

When the user is dragging the border node, to determine the side on which the border node will snap, the algorithm calculates the distance between the center of the dragged border node to each side segment of the parent node. The snap is done on the closest parent node side.

Then the offset is applied on the border node to have the border node entering the parent node

== Status

WIP

== Consequences
8 changes: 6 additions & 2 deletions frontend/src/diagram/DiagramWebSocketContainer.tsx
Expand Up @@ -106,7 +106,7 @@ import { Toolbar } from 'diagram/Toolbar';
import { atLeastOneCanInvokeEdgeTool, canInvokeTool } from 'diagram/toolServices';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { SelectionDialogWebSocketContainer } from 'selection/SelectionDialogWebSocketContainer';
import { EditLabelAction, HoverFeedbackAction, SEdge, SGraph, SModelElement, SNode } from 'sprotty';
import { EditLabelAction, HoverFeedbackAction, SEdge, SGraph, SModelElement, SNode, SPort } from 'sprotty';
import { FitToScreenAction } from 'sprotty-protocol';
import { v4 as uuid } from 'uuid';
import { RepresentationComponentProps, Selection, SelectionEntry } from 'workbench/Workbench.types';
Expand Down Expand Up @@ -568,7 +568,11 @@ export const DiagramWebSocketContainer = ({
if (selectedElement instanceof SGraph) {
const { id, label, kind } = selectedElement as any; // (as any) to be removed when the proper type will be available
newSelection.entries.push({ id, label, kind });
} else if (selectedElement instanceof SNode || selectedElement instanceof SEdge) {
} else if (
selectedElement instanceof SNode ||
selectedElement instanceof SPort ||
selectedElement instanceof SEdge
) {
const { id, kind, targetObjectId, targetObjectKind, targetObjectLabel } = selectedElement as any; // (as any) to be removed when the proper type will be available

const semanticSelectionEntry: SelectionEntry = {
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/diagram/sprotty/DependencyInjection.ts
@@ -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 @@ -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 @@ -119,7 +119,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 Expand Up @@ -229,9 +231,14 @@ export const createDependencyInjectionContainer = (containerId: string, getCurso
}
}

const findModelElementWithSemanticTarget = (element: SModelElement): SGraph | Node | SEdge | null => {
let graphicalElement: SGraph | Node | SEdge | null = null;
if (element instanceof SGraph || element instanceof Node || element instanceof SEdge) {
const findModelElementWithSemanticTarget = (element: SModelElement): SGraph | Node | BorderNode | SEdge | null => {
let graphicalElement: SGraph | Node | BorderNode | SEdge | null = null;
if (
element instanceof SGraph ||
element instanceof Node ||
element instanceof BorderNode ||
element instanceof SEdge
) {
graphicalElement = element;
} else if (element instanceof SLabel) {
graphicalElement = findModelElementWithSemanticTarget(element.parent);
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 {}
131 changes: 131 additions & 0 deletions frontend/src/diagram/sprotty/__tests__/borderNodes.test.ts
@@ -0,0 +1,131 @@
/*******************************************************************************
* Copyright (c) 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
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { httpOrigin } from 'common/URL';
import 'reflect-metadata';
import { SNode } from 'sprotty';
import { Point } from 'sprotty-protocol/';
import { convertDiagram } from '../convertDiagram';
import { BorderNode } from '../Diagram.types';
import { SiriusDragAndDropMouseListener } from '../siriusDragAndDropMouseListener';
import { siriusWebDiagram } from './siriusWebDiagram';

describe('Border node positioning', () => {
it('snaps the border node', () => {
const sprottyDiagram = convertDiagram(siriusWebDiagram, httpOrigin, false);

const sprottyNode: SNode = <SNode>sprottyDiagram.children[1];

const borderNode: BorderNode = <BorderNode>sprottyNode.children.filter((value) => {
return value instanceof BorderNode;
})[0];

// border node position when its center is positionned at the parent upper left corner(origin))
const borderNodeOrigin: Point = { x: -borderNode.size.width / 2, y: -borderNode.size.height / 2 };
const parentWidth: number = sprottyNode.size.width;
const parentHeight: number = sprottyNode.size.height;

const siriusDragAndDropMouseListener: SiriusDragAndDropMouseListener = new SiriusDragAndDropMouseListener();
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x - 10, y: borderNodeOrigin.y - 5 }, borderNode, false)
).toStrictEqual({ x: -52, y: -20 });
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x - 10, y: borderNodeOrigin.y - 15 }, borderNode, false)
).toStrictEqual({ x: -30, y: -32 });
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x + 50, y: borderNodeOrigin.y - 25 }, borderNode, false)
).toStrictEqual({ x: 20, y: -32 });
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x + 50, y: borderNodeOrigin.y + 5 }, borderNode, false)
).toStrictEqual({ x: 20, y: -32 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth + 5, y: borderNodeOrigin.y - 10 },
borderNode,
false
)
).toStrictEqual({ x: 250, y: -32 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth + 15, y: borderNodeOrigin.y - 10 },
borderNode,
false
)
).toStrictEqual({ x: 272, y: -20 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth + 15, y: borderNodeOrigin.y + 50 },
borderNode,
false
)
).toStrictEqual({ x: 272, y: 30 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth - 5, y: borderNodeOrigin.y + 50 },
borderNode,
false
)
).toStrictEqual({ x: 272, y: 30 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth + 15, y: borderNodeOrigin.y + parentHeight + 10 },
borderNode,
false
)
).toStrictEqual({ x: 272, y: 160 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth + 10, y: borderNodeOrigin.y + parentHeight + 15 },
borderNode,
false
)
).toStrictEqual({ x: 250, y: 172 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth / 2, y: borderNodeOrigin.y + parentHeight - 5 },
borderNode,
false
)
).toStrictEqual({ x: 110, y: 172 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + parentWidth / 2, y: borderNodeOrigin.y + parentHeight + 5 },
borderNode,
false
)
).toStrictEqual({ x: 110, y: 172 });

expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x - 5, y: borderNodeOrigin.y + 10 }, borderNode, false)
).toStrictEqual({ x: -52, y: -10 });
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x - 10, y: borderNodeOrigin.y + 5 }, borderNode, false)
).toStrictEqual({ x: -52, y: -15 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x + 10, y: borderNodeOrigin.y + parentHeight / 2 },
borderNode,
false
)
).toStrictEqual({ x: -52, y: 70 });
expect(
siriusDragAndDropMouseListener.snap(
{ x: borderNodeOrigin.x - 10, y: borderNodeOrigin.y + parentHeight / 2 },
borderNode,
false
)
).toStrictEqual({ x: -52, y: 70 });
expect(
siriusDragAndDropMouseListener.snap({ x: borderNodeOrigin.x - 10, y: borderNodeOrigin.y - 5 }, borderNode, false)
).toStrictEqual({ x: -52, y: -20 });
});
});
8 changes: 4 additions & 4 deletions frontend/src/diagram/sprotty/__tests__/siriusWebDiagram.ts
Expand Up @@ -134,8 +134,8 @@ export const siriusWebDiagram: GQLDiagram = {
y: 64.34101572060084,
},
size: {
width: 286,
height: 181.6982421875,
width: 280,
height: 180,
},
borderNodes: [
{
Expand Down Expand Up @@ -181,8 +181,8 @@ export const siriusWebDiagram: GQLDiagram = {
y: 139.6982421875,
},
size: {
width: 30,
height: 30,
width: 60,
height: 40,
},
borderNodes: [],
childNodes: [],
Expand Down
29 changes: 28 additions & 1 deletion frontend/src/diagram/sprotty/convertDiagram.tsx
Expand Up @@ -25,6 +25,7 @@ import {
} from 'diagram/DiagramWebSocketContainer.types';
import {
ArrowStyle,
BorderNode,
Diagram,
Edge,
EdgeStyle,
Expand Down Expand Up @@ -120,7 +121,7 @@ const convertNode = (
parentElement.add(node);

const convertedLabel = convertLabel(node, label, httpOrigin, readOnly);
(borderNodes ?? []).map((borderNode) => convertNode(node, borderNode, httpOrigin, readOnly, autoLayout));
(borderNodes ?? []).map((borderNode) => convertBorderNode(node, borderNode, httpOrigin, readOnly, autoLayout));
(childNodes ?? []).map((childNode) => convertNode(node, childNode, httpOrigin, readOnly, autoLayout));

node.id = id;
Expand All @@ -138,6 +139,32 @@ const convertNode = (

return node;
};
const convertBorderNode = (
parentElement: SParentElement,
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();
parentElement.add(node);

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

0 comments on commit 33a1d0d

Please sign in to comment.