Skip to content

Commit

Permalink
#738: Add route to ComputedBoundsAction
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-fleck-at committed Nov 9, 2022
1 parent 4a5a0ee commit 0f2e031
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 17 deletions.
4 changes: 2 additions & 2 deletions packages/client/src/base/container-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import { Container, ContainerModule } from 'inversify';
import {
boundsModule,
buttonModule,
defaultModule,
edgeIntersectionModule,
Expand All @@ -30,6 +29,7 @@ import {
routingModule,
zorderModule
} from 'sprotty';
import glspBoundsModule from '../features/bounds/di.config';
import glspCommandPaletteModule from '../features/command-palette/di.config';
import glspContextMenuModule from '../features/context-menu/di.config';
import { glspServerCopyPasteModule } from '../features/copy-paste/di.config';
Expand All @@ -52,13 +52,13 @@ import defaultGLSPModule from './di.config';
export const DEFAULT_MODULES = [
defaultModule,
defaultGLSPModule,
boundsModule,
buttonModule,
edgeIntersectionModule,
edgeLayoutModule,
expandModule,
exportModule,
fadeModule,
glspBoundsModule,
glspCommandPaletteModule,
glspContextMenuModule,
glspDecorationModule,
Expand Down
39 changes: 39 additions & 0 deletions packages/client/src/features/bounds/di.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/********************************************************************************
* Copyright (c) 2022 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { ContainerModule } from 'inversify';
import {
configureCommand, configureLayout, HBoxLayouter, HiddenBoundsUpdater, Layouter,
LayoutRegistry, RequestBoundsCommand, SetBoundsCommand, StackLayouter, VBoxLayouter
} from 'sprotty';
import '../../../css/command-palette.css';
import { TYPES } from '../../base/types';
import { GLSPHiddenBoundsUpdater } from './glsp-hidden-bounds-updater';

const glspBoundsModule = new ContainerModule((bind, _unbind, isBound, _rebind) => {
configureCommand({ bind, isBound }, SetBoundsCommand);
configureCommand({ bind, isBound }, RequestBoundsCommand);
bind(HiddenBoundsUpdater).toSelf().inSingletonScope();
bind(GLSPHiddenBoundsUpdater).toSelf().inSingletonScope();
bind(TYPES.HiddenVNodePostprocessor).toService(GLSPHiddenBoundsUpdater);
bind(TYPES.Layouter).to(Layouter).inSingletonScope();
bind(TYPES.LayoutRegistry).to(LayoutRegistry).inSingletonScope();

configureLayout({ bind, isBound }, VBoxLayouter.KIND, VBoxLayouter);
configureLayout({ bind, isBound }, HBoxLayouter.KIND, HBoxLayouter);
configureLayout({ bind, isBound }, StackLayouter.KIND, StackLayouter);
});

export default glspBoundsModule;
88 changes: 88 additions & 0 deletions packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/********************************************************************************
* Copyright (c) 2022 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import {
Action, ComputedBoundsAction, Deferred, ElementAndRoutingPoints, RequestAction,
ResponseAction
} from '@eclipse-glsp/protocol';
import { inject, injectable, optional } from 'inversify';
import { VNode } from 'snabbdom';
import { EdgeRouterRegistry, HiddenBoundsUpdater, IActionDispatcher, SModelElement, SRoutableElement } from 'sprotty';
import { calcElementAndRoute, isRoutable } from '../../utils/smodel-util';

/**
* Grabs the bounds from hidden SVG DOM elements, applies layouts, collects routes and fires {@link ComputedBoundsAction}s.
*
* The actions will contain the bound, alignment, and routing points of elements.
*/
@injectable()
export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater {

@inject(EdgeRouterRegistry) @optional() protected readonly edgeRouterRegistry?: EdgeRouterRegistry;

protected element2route: ElementAndRoutingPoints[] = [];
protected edges: SRoutableElement[] = [];
protected nodes: VNode[] = [];

override decorate(vnode: VNode, element: SModelElement): VNode {
super.decorate(vnode, element);
if (isRoutable(element)) {
this.element2route.push(calcElementAndRoute(element, this.edgeRouterRegistry));
}
return vnode;
}

override postUpdate(cause?: Action): void {
const actions = this.captureActions(() => super.postUpdate(cause));
actions.filter(action => ComputedBoundsAction.is(action))
.forEach(action => this.actionDispatcher.dispatch(this.enhanceAction(action as ComputedBoundsAction)));
this.element2route = [];
}

protected captureActions(call: () => void): Action[] {
const capturingActionDispatcher = new CapturingActionDispatcher();
const actualActionDispatcher = this.actionDispatcher;
this.actionDispatcher = capturingActionDispatcher;
try {
call();
return capturingActionDispatcher.actions;
} finally {
this.actionDispatcher = actualActionDispatcher;
}
}

protected enhanceAction(action: ComputedBoundsAction): ComputedBoundsAction {
action.routes = this.element2route.length === 0 ? undefined : this.element2route;
return action;
}
}

class CapturingActionDispatcher implements IActionDispatcher {
readonly actions: Action[] = [];

async dispatch(action: Action): Promise<void> {
this.actions.push(action);
}

async dispatchAll(actions: Action[]): Promise<void> {
this.actions.push(...actions);
}

async request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
// ignore, not needed for our purposes
return new Deferred<Res>().promise;
}
}
56 changes: 45 additions & 11 deletions packages/client/src/utils/smodel-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { distinctAdd, ElementAndBounds, ElementAndRoutingPoints, remove, SModelElementSchema, TypeGuard } from '@eclipse-glsp/protocol';
import { distinctAdd, ElementAndBounds, ElementAndRoutingPoints, Point, remove, SModelElementSchema, TypeGuard } from '@eclipse-glsp/protocol';
import {
BoundsAware,
EdgeRouterRegistry,
isBoundsAware,
isMoveable,
isSelectable,
isSelected,
ModelIndexImpl,
isSelected, ModelIndexImpl,
RoutedPoint,
Selectable,
SModelElement,
SRoutableElement,
Expand Down Expand Up @@ -226,21 +226,55 @@ export function toElementAndRoutingPoints(element: SRoutableElement): ElementAnd
};
}

/** Pure routing point data kinds. */
export const ROUTING_POINT_KINDS = ['linear', 'bezier-junction'];

/** Pure route data kinds. */
export const ROUTE_KINDS = [...ROUTING_POINT_KINDS, 'source', 'target'];

/**
* Helper function to calculate the {@link ElementAndRoutingPoints} for a given {@link SRoutableElement}.
* If client layout is activated, i.e., the edge routing registry is given and has a router for the element, then the routing
* points from the calculated route are used, otherwise we use the already specified routing points of the {@link SRoutableElement}.
* @param element The element to translate.
* @param edgeRouterRegistry the edge router registry
* @param routerRegistry the edge router registry.
* @returns The corresponding {@link ElementAndRoutingPoints} for the given element.
*/
export function calcElementAndRoutingPoints(element: SRoutableElement, edgeRouterRegistry?: EdgeRouterRegistry): ElementAndRoutingPoints {
const router = edgeRouterRegistry?.get(element.routerKind);
// filter duplicate points (same x,y coordinates) and only keep actual routing points (no start or target, no volatile points)
const clientRoutingPoints = router?.route(element)
.filter((point, idx, route) => idx === route.findIndex(otherPoint => otherPoint.x === point.x && otherPoint.y === point.y))
.filter(point => point.kind === 'linear' || point.kind === 'bezier-junction');
return { elementId: element.id, newRoutingPoints: clientRoutingPoints || element.routingPoints };
export function calcElementAndRoutingPoints(element: SRoutableElement, routerRegistry?: EdgeRouterRegistry): ElementAndRoutingPoints {
const newRoutingPoints = routerRegistry ? calcRoute(element, routerRegistry, ROUTING_POINT_KINDS) : element.routingPoints;
return { elementId: element.id, newRoutingPoints };
}

/**
* Helper function to calculate the route for a given {@link SRoutableElement}.
* If client layout is activated, i.e., the edge routing registry is given and has a router for the element, then the points
* from the calculated route are used, otherwise we use the already specified routing points of the {@link SRoutableElement}.
* @param element The element to translate.
* @param routerRegistry the edge router registry.
* @returns The corresponding route for the given element.
*/
export function calcElementAndRoute(element: SRoutableElement, routerRegistry?: EdgeRouterRegistry): ElementAndRoutingPoints {
let route: Point[] | undefined = routerRegistry ? calcRoute(element, routerRegistry, ROUTE_KINDS) : undefined;
if (!route) {
// add source and target to the routing points
route = [...element.routingPoints];
route.splice(0, 0, element.source?.position || Point.ORIGIN);
route.push(element.target?.position || Point.ORIGIN);
}
return { elementId: element.id, newRoutingPoints: route };
}

/**
* Helper function to calculate the route for a given {@link SRoutableElement}.
* @param element The element to translate.
* @param routerRegistry the edge router registry.
* @param pointKinds the routing point kinds that should be considered.
* @returns The corresponding route for the given element.
*/
export function calcRoute(element: SRoutableElement, routerRegistry: EdgeRouterRegistry, pointKinds?: string[]): RoutedPoint[] | undefined {
return routerRegistry?.get(element.routerKind).route(element)
.filter((point, idx, fullRoute) => idx === fullRoute.findIndex(otherPoint => otherPoint.x === point.x && otherPoint.y === point.y))
.filter(point => !pointKinds || pointKinds.includes(point.kind));
}

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/protocol/src/action-protocol/model-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ describe('Model layout actions', () => {
responseId: 'myResponse',
alignments: [{ elementId: 'myElement', newAlignment: Point.ORIGIN }],
revision: 5,
bounds: [{ elementId: '', newSize: Dimension.EMPTY, newPosition: Point.ORIGIN }]
bounds: [{ elementId: '', newSize: Dimension.EMPTY, newPosition: Point.ORIGIN }],
routes: [{ elementId: 'myEdge', newRoutingPoints: [{ x: 42, y: 1337 }] }]
};
const { bounds, responseId, alignments, revision } = expected;
expect(ComputedBoundsAction.create(bounds, { responseId, alignments, revision })).to.deep.equals(expected);
const { bounds, responseId, alignments, revision, routes } = expected;
expect(ComputedBoundsAction.create(bounds, { responseId, alignments, revision, routes })).to.deep.equals(expected);
});
});
});
Expand Down
8 changes: 7 additions & 1 deletion packages/protocol/src/action-protocol/model-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as sprotty from 'sprotty-protocol/lib/actions';
import { hasArrayProp, hasObjectProp } from '../utils/type-util';
import { Action, Operation, RequestAction, ResponseAction } from './base-protocol';
import { SModelRootSchema } from './model-structure';
import { ElementAndAlignment, ElementAndBounds } from './types';
import { ElementAndAlignment, ElementAndBounds, ElementAndRoutingPoints } from './types';

/**
* Sent from the server to the client to request bounds for the given model. The model is rendered invisibly so the bounds can
Expand Down Expand Up @@ -74,6 +74,11 @@ export interface ComputedBoundsAction extends ResponseAction, sprotty.ComputedBo
* The new alignment of the model elements.
*/
alignments?: ElementAndAlignment[];

/**
* The route of the model elements.
*/
routes?: ElementAndRoutingPoints[];
}

export namespace ComputedBoundsAction {
Expand All @@ -89,6 +94,7 @@ export namespace ComputedBoundsAction {
revision?: number;
responseId?: string;
alignments?: ElementAndAlignment[];
routes?: ElementAndRoutingPoints[];
} = {}
): ComputedBoundsAction {
return {
Expand Down

0 comments on commit 0f2e031

Please sign in to comment.