Skip to content

Commit 4f29c67

Browse files
authored
GLSP-394: NoOverlapMovementRestrictor support for nested nodes (#452)
Adapt the implementation of the NoOverlapMovementRestrictor to properly handle nested nodes Fixes eclipse-glsp/glsp#394
1 parent eb23ff3 commit 4f29c67

File tree

3 files changed

+134
-22
lines changed

3 files changed

+134
-22
lines changed

examples/workflow-glsp/src/workflow-diagram-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2019-2024 EclipseSource and others.
2+
* Copyright (c) 2019-2025 EclipseSource and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -30,6 +30,7 @@ import {
3030
GLabelView,
3131
IHelperLineOptions,
3232
LogLevel,
33+
NoOverlapMovementRestrictor,
3334
RectangularNodeView,
3435
RevealNamedElementActionProvider,
3536
RoundedCornerNodeView,
@@ -64,7 +65,7 @@ export const workflowDiagramModule = new FeatureModule(
6465
bindOrRebind(context, TYPES.LogLevel).toConstantValue(LogLevel.warn);
6566
bindAsService(context, TYPES.ICommandPaletteActionProvider, RevealNamedElementActionProvider);
6667
bindAsService(context, TYPES.IContextMenuItemProvider, DeleteElementContextMenuItemProvider);
67-
68+
bind(TYPES.IMovementRestrictor).to(NoOverlapMovementRestrictor).inSingletonScope();
6869
configureDefaultModelElements(context);
6970
configureModelElement(context, 'task:automated', TaskNode, RoundedCornerNodeView);
7071
configureModelElement(context, 'task:manual', TaskNode, RoundedCornerNodeView);

packages/client/src/features/change-bounds/movement-restrictor.ts

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2019-2024 EclipseSource and others.
2+
* Copyright (c) 2019-2025 EclipseSource and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -13,11 +13,23 @@
1313
*
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
16-
import { Bounds, Dimension, GModelElement, GNode, GParentElement, Point, isBoundsAware, isMoveable } from '@eclipse-glsp/sprotty';
16+
import {
17+
Bounds,
18+
Dimension,
19+
GChildElement,
20+
GModelElement,
21+
GNode,
22+
GParentElement,
23+
isBoundsAware,
24+
isMoveable,
25+
Point,
26+
toTypeGuard
27+
} from '@eclipse-glsp/sprotty';
1728
import { injectable } from 'inversify';
18-
import { ModifyCSSFeedbackAction } from '../../base/feedback/css-feedback';
19-
import { BoundsAwareModelElement } from '../../utils/gmodel-util';
29+
import { CSS_GHOST_ELEMENT, ModifyCSSFeedbackAction } from '../../base/feedback/css-feedback';
30+
import { BoundsAwareModelElement, getChildren, getParents } from '../../utils/gmodel-util';
2031
import { toAbsoluteBounds } from '../../utils/viewpoint-util';
32+
import { ContainerElement, isContainable } from '../hints/model';
2133
import { GResizeHandle } from './model';
2234

2335
/**
@@ -39,34 +51,75 @@ export interface IMovementRestrictor {
3951
cssClasses?: string[];
4052
}
4153

54+
export interface MoveElementContext {
55+
element: GModelElement;
56+
elementAtNewLocation: BoundsAwareModelElement;
57+
parentContainers: ContainerElement[];
58+
childNodes: GNode[];
59+
}
60+
4261
/**
4362
* A `IMovementRestrictor` that checks for overlapping elements. Move operations
44-
* are only valid if the element does not collide with another element after moving.
63+
* are only valid if the element does not collide with another element/node after moving.
4564
*/
4665
@injectable()
4766
export class NoOverlapMovementRestrictor implements IMovementRestrictor {
4867
cssClasses = ['movement-not-allowed'];
4968

5069
validate(element: GModelElement, newLocation?: Point): boolean {
51-
if (!isMoveable(element) || !newLocation) {
70+
if (!(element instanceof GChildElement) || !isMoveable(element) || !newLocation) {
5271
return false;
5372
}
54-
// Create ghost element at the newLocation
73+
74+
const moveContext = this.createMoveElementContext(element, newLocation);
75+
const elementsToValidate = Array.from(element.root.index.all()).filter(e =>
76+
this.isBoundsRelevant(e, moveContext)
77+
) as BoundsAwareModelElement[];
78+
79+
const valid = !elementsToValidate.some(e => this.areOverlapping(e, moveContext.elementAtNewLocation));
80+
return valid;
81+
}
82+
83+
protected createMoveElementContext(element: GModelElement, newLocation: Point): MoveElementContext {
84+
const parentContainers = getParents(element, isContainable);
85+
const childNodes = getChildren(element, toTypeGuard(GNode));
86+
// Create a mock element at the newLocation for overlap checking
5587
const dimensions: Dimension = isBoundsAware(element) ? element.bounds : { width: 1, height: 1 };
56-
const ghostElement = Object.create(element) as BoundsAwareModelElement;
57-
ghostElement.bounds = { ...dimensions, ...newLocation };
58-
ghostElement.type = 'Ghost';
59-
ghostElement.id = element.id;
60-
return !Array.from(
61-
element.root.index
62-
.all()
63-
.filter(node => node.id !== ghostElement.id && node !== ghostElement.root && node instanceof GNode)
64-
.map(node => node as BoundsAwareModelElement)
65-
).some(e => this.areOverlapping(e, ghostElement));
88+
const elementAtNewLocation = Object.create(element) as BoundsAwareModelElement as BoundsAwareModelElement;
89+
elementAtNewLocation.bounds = { ...dimensions, ...newLocation };
90+
91+
return {
92+
element,
93+
elementAtNewLocation,
94+
parentContainers,
95+
childNodes
96+
};
6697
}
6798

68-
protected isBoundsRelevant(element: GModelElement, ghostElement: BoundsAwareModelElement): element is BoundsAwareModelElement {
69-
return element.id !== ghostElement.id && element !== ghostElement.root && element instanceof GNode && isBoundsAware(element);
99+
protected isBoundsRelevant(element: GModelElement, moveContext: MoveElementContext): element is BoundsAwareModelElement {
100+
// Only consider GNodes that are not the element being moved (or one of its children)
101+
if (
102+
!(element instanceof GNode) ||
103+
element.id === moveContext.element.id ||
104+
moveContext.childNodes.some(child => child.id === element.id)
105+
) {
106+
return false;
107+
}
108+
109+
// Do not consider parent containers of the element being moved
110+
if (moveContext.parentContainers.length > 0 && moveContext.parentContainers.some(container => container.id === element.id)) {
111+
return false;
112+
}
113+
114+
// If the element is a ghost element (node creation), don't consider overlap checks for potential parent containers
115+
if (
116+
moveContext.element.cssClasses?.includes(CSS_GHOST_ELEMENT) &&
117+
isContainable(element) &&
118+
element.isContainableElement(moveContext.element.type)
119+
) {
120+
return false;
121+
}
122+
return true;
70123
}
71124

72125
protected areOverlapping(element1: BoundsAwareModelElement, element2: BoundsAwareModelElement): boolean {

packages/client/src/utils/gmodel-util.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2019-2024 EclipseSource and others.
2+
* Copyright (c) 2019-2025 EclipseSource and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -86,6 +86,63 @@ export function getMatchingElements<T>(index: ModelIndexImpl, predicate: ModelFi
8686
return Array.from(filter(index, predicate));
8787
}
8888

89+
/**
90+
* Retrieves an array of all parent elements for the given element that match the given type guard.
91+
* @param element The {@link GModelElement} for which the parent elements should be retrieved.
92+
* @param guard The type guard that should be used.
93+
* @returns An array of all parent elements that match the type guard
94+
* (correctly casted to also include the additional type of the guard).
95+
*/
96+
export function getParents<T extends GParentElement>(element: GModelElement, guard: TypeGuard<T>): T[];
97+
/**
98+
* Retrieves an array of all parent elements for the given element that match the given {@link ModelFilterPredicate}.
99+
* @param element The {@link GModelElement} for which the parent elements should be retrieved.
100+
* @param predicate The filter predicate that should be used.
101+
* @returns An array of all parent elements that match the filter predicate
102+
* (correctly casted to also include the additional type of the predicate).
103+
*/
104+
export function getParents<T>(element: GModelElement, predicate: ModelFilterPredicate<T>): (GParentElement & T)[];
105+
export function getParents(element: GModelElement, filterPredicate: (parent: GParentElement) => boolean): GParentElement[] {
106+
const parents: GParentElement[] = [];
107+
let current: GModelElement | undefined = element;
108+
while (current instanceof GChildElement) {
109+
const parent: GParentElement = current.parent;
110+
if (filterPredicate(parent)) {
111+
parents.push(parent);
112+
}
113+
current = parent;
114+
}
115+
return parents;
116+
}
117+
118+
/** * Retrieves an array of all child elements for the given element that match the given type guard.
119+
* @param element The {@link GModelElement} for which the child elements should be retrieved.
120+
* @param guard The type guard that should be used.
121+
* @returns An array of all child elements that match the type guard
122+
* (correctly casted to also include the additional type of the guard).
123+
*/
124+
export function getChildren<T extends GChildElement>(element: GModelElement, guard: TypeGuard<T>): T[];
125+
/** * Retrieves an array of all child elements for the given element that match the given {@link ModelFilterPredicate}.
126+
* @param element The {@link GModelElement} for which the child elements should be retrieved.
127+
* @param predicate The filter predicate that should be used.
128+
* @returns An array of all child elements that match the filter predicate
129+
* (correctly casted to also include the additional type of the predicate).
130+
*/
131+
export function getChildren<T>(element: GModelElement, predicate: ModelFilterPredicate<T>): (GChildElement & T)[];
132+
export function getChildren(element: GModelElement, filterPredicate: (parent: GChildElement) => boolean): GChildElement[] {
133+
const children: GChildElement[] = [];
134+
if (element instanceof GParentElement) {
135+
for (const child of element.children) {
136+
if (filterPredicate(child)) {
137+
children.push(child);
138+
}
139+
if (child instanceof GParentElement) {
140+
children.push(...getChildren(child, filterPredicate as any));
141+
}
142+
}
143+
}
144+
return children;
145+
}
89146
/**
90147
* Invokes the given model index to retrieve the corresponding model elements for the given set of ids. Ids that
91148
* have no corresponding element in the index will be ignored.
@@ -104,6 +161,7 @@ export function getElements<S extends GModelElement>(index: ModelIndexImpl, elem
104161
};
105162
return elementsIDs.map(id => index.getById(id)).filter(filterFn);
106163
}
164+
107165
/**
108166
* Retrieves the amount of currently selected elements in the given {@link ModelIndexImpl}.
109167
* @param index The {@link ModelIndexImpl}.

0 commit comments

Comments
 (0)