Skip to content

Commit 9401d56

Browse files
committed
Improved diagram server API
1 parent ddf634c commit 9401d56

File tree

11 files changed

+129
-68
lines changed

11 files changed

+129
-68
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
node_modules/
44
npm-debug.log
55
yarn-error.log
6+
.DS_Store

examples/random-graph/src/di.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import {
2121
loadDefaultModules, LocalModelSource, SNode, SEdge, SLabel, configureModelElement,
2222
SGraph, RectangularNodeView, PolylineEdgeView
2323
} from 'sprotty';
24-
import { ElkFactory, ElkLayoutEngine } from 'sprotty-elk';
25-
import elkLayoutModule from 'sprotty-elk/lib/di.config';
24+
import { ElkFactory, ElkLayoutEngine, elkLayoutModule } from 'sprotty-elk/lib/inversify';
2625

2726
export default (containerId: string) => {
2827
require('sprotty/css/sprotty.css');

packages/sprotty-elk/CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ This change log covers only the `elkjs` layout of Sprotty. See also the change l
66

77
The `sprotty-elk` package was moved to the main repository of Sprotty, which is now a monorepo. Furthermore, the dependency to the `sprotty` package was removed in favor of the new `sprotty-protocol` package.
88

9-
This package can now be used on the backend side with Node.js as well as on the frontend side.
9+
This package can now be used on the backend side with Node.js as well as on the frontend side:
10+
* In the backend, import from `'sprotty-elk/lib/elk-layout'`. These are the plain definitions without a dependency to InversifyJS.
11+
* In the frontend, import from `'sprotty-elk'` (the default export) or `'sprotty-elk/lib/inversify'`. These definitions are adapted to be used with InversifyJS.
12+
13+
Of course you can use the InversifyJS-specific definitions in the backend as well if you plan to use that DI framework there.
1014

1115
Breaking API changes:
12-
* The `elkLayoutModule` is no longer exported by the package index. Imports need to be changed to this:
13-
```typescript
14-
import elkLayoutModule from 'sprotty-elk/lib/di.config';
15-
```
16+
* The module `di.config` was removed in favor of `inversify`, which contains all InversifyJS-specific definitions.
17+
* The plain definitions in the module `elk-layout` no longer include InversifyJS annotations.
1618
* The `elkLayoutModule` no longer includes a binding for `TYPES.IModelLayoutEngine`. Add it like this:
1719
```typescript
1820
bind(TYPES.IModelLayoutEngine).toService(ElkLayoutEngine);

packages/sprotty-elk/src/elk-layout.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import { expect } from 'chai';
2020
import { Container } from 'inversify';
2121
import ElkConstructor from 'elkjs/lib/elk.bundled';
2222
import { SEdge, SGraph, SNode, SPort } from 'sprotty-protocol/lib/model';
23-
import { ElkFactory, ElkLayoutEngine } from './elk-layout';
24-
import elkLayoutModule from './di.config';
23+
import { ElkFactory, ElkLayoutEngine, elkLayoutModule } from './inversify';
2524

2625
describe('ElkLayoutEngine', () => {
2726
const container = new Container();

packages/sprotty-elk/src/elk-layout.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2018 TypeFox and others.
2+
* Copyright (c) 2018-2021 TypeFox 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
@@ -14,31 +14,25 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { injectable, inject } from 'inversify';
1817
import {
1918
ELK, ElkNode, ElkGraphElement, ElkEdge, ElkLabel, ElkPort, ElkShape, ElkPrimitiveEdge,
2019
ElkExtendedEdge, LayoutOptions
2120
} from 'elkjs/lib/elk-api';
22-
import { IModelLayoutEngine } from 'sprotty-protocol/lib/diagram-server';
21+
import { IModelLayoutEngine } from 'sprotty-protocol/lib/diagram-services';
2322
import { SEdge, SGraph, SLabel, SModelElement, SNode, SPort, SShapeElement } from 'sprotty-protocol/lib/model';
2423
import { Point } from 'sprotty-protocol/lib/utils/geometry';
2524
import { getBasicType, SModelIndex } from 'sprotty-protocol/lib/utils/model-utils';
2625

27-
export const ElkFactory = Symbol('ElkFactory');
28-
export const IElementFilter = Symbol('IElementFilter');
29-
export const ILayoutConfigurator = Symbol('ILayoutConfigurator');
30-
3126
/**
3227
* Layout engine that delegates to ELK by transforming the Sprotty graph into an ELK graph.
3328
*/
34-
@injectable()
3529
export class ElkLayoutEngine implements IModelLayoutEngine {
3630

3731
protected readonly elk: ELK;
3832

39-
constructor(@inject(ElkFactory) elkFactory: ElkFactory,
40-
@inject(IElementFilter) protected readonly filter: IElementFilter,
41-
@inject(ILayoutConfigurator) protected readonly configurator: ILayoutConfigurator) {
33+
constructor(elkFactory: ElkFactory,
34+
protected readonly filter: IElementFilter,
35+
protected readonly configurator: ILayoutConfigurator) {
4236
this.elk = elkFactory();
4337
}
4438

@@ -272,7 +266,6 @@ export interface IElementFilter {
272266
apply(element: SModelElement, index: SModelIndex): boolean
273267
}
274268

275-
@injectable()
276269
export class DefaultElementFilter implements IElementFilter {
277270

278271
apply(element: SModelElement, index: SModelIndex): boolean {
@@ -329,7 +322,6 @@ export interface ILayoutConfigurator {
329322
apply(element: SModelElement, index: SModelIndex): LayoutOptions | undefined
330323
}
331324

332-
@injectable()
333325
export class DefaultLayoutConfigurator implements ILayoutConfigurator {
334326

335327
apply(element: SModelElement, index: SModelIndex): LayoutOptions | undefined {

packages/sprotty-elk/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
export * from './elk-layout';
17+
// By default we export the Inversify-ready version. If you want to use the plain
18+
// version, import from `sprotty-elk/lib/elk-layout`.
19+
export * from './inversify';

packages/sprotty-elk/src/di.config.ts renamed to packages/sprotty-elk/src/inversify.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2018 TypeFox and others.
2+
* Copyright (c) 2018-2021 TypeFox 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
@@ -14,11 +14,26 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { ContainerModule } from 'inversify';
17+
import { ContainerModule, injectable } from 'inversify';
1818
import {
19-
ElkLayoutEngine, DefaultElementFilter, IElementFilter, DefaultLayoutConfigurator, ILayoutConfigurator
19+
ElkLayoutEngine as ElkLayoutEnginePlain, DefaultElementFilter as DefaultElementFilterPlain,
20+
DefaultLayoutConfigurator as DefaultLayoutConfiguratorPlain, ElkFactory as ElkFactoryPlain,
21+
IElementFilter as IElementFilterPlain, ILayoutConfigurator as ILayoutConfiguratorPlain
2022
} from './elk-layout';
2123

24+
export const ElkLayoutEngine: typeof ElkLayoutEnginePlain = injectable()(ElkLayoutEnginePlain);
25+
26+
export type ElkFactory = ElkFactoryPlain;
27+
export const ElkFactory = Symbol('ElkFactory');
28+
29+
export type IElementFilter = IElementFilterPlain;
30+
export const IElementFilter = Symbol('IElementFilter');
31+
export const DefaultElementFilter: typeof DefaultElementFilterPlain = injectable()(DefaultElementFilterPlain);
32+
33+
export type ILayoutConfigurator = ILayoutConfiguratorPlain;
34+
export const ILayoutConfigurator = Symbol('ILayoutConfigurator');
35+
export const DefaultLayoutConfigurator: typeof DefaultLayoutConfiguratorPlain = injectable()(DefaultLayoutConfiguratorPlain);
36+
2237
/**
2338
* This dependency injection module adds the default bindings for the frontend integration of ELK.
2439
* **Note:** Since this package has no direct dependency to the `sprotty` frontend package,
@@ -35,10 +50,13 @@ import {
3550
* You can import `ElkConstructor` from `'elkjs/lib/elk.bundled'` for the bundled variant or
3651
* `'elkjs/lib/elk-api'` for the webworker variant.
3752
*/
38-
const elkLayoutModule = new ContainerModule(bind => {
39-
bind(ElkLayoutEngine).toSelf().inSingletonScope();
53+
export const elkLayoutModule = new ContainerModule(bind => {
54+
bind(ElkLayoutEngine).toDynamicValue(context => {
55+
const elkFactory = context.container.get<ElkFactory>(ElkFactory);
56+
const elementFilter = context.container.get<IElementFilter>(IElementFilter);
57+
const layoutConfigurator = context.container.get<ILayoutConfigurator>(ILayoutConfigurator);
58+
return new ElkLayoutEngine(elkFactory, elementFilter, layoutConfigurator);
59+
}).inSingletonScope();
4060
bind(IElementFilter).to(DefaultElementFilter);
4161
bind(ILayoutConfigurator).to(DefaultLayoutConfigurator);
4262
});
43-
44-
export default elkLayoutModule;

packages/sprotty-protocol/src/diagram-server.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ describe('DiagramServer', () => {
4545
async a => {
4646
dispatched.push(a)
4747
}, {
48-
diagramGenerator: {
48+
DiagramGenerator: {
4949
generate: () => {
5050
return {
5151
type: 'root',
5252
id: 'root',
5353
};
5454
}
5555
},
56-
layoutEngine: {
56+
ModelLayoutEngine: {
5757
layout: model => {
5858
(model as SModelRoot & BoundsAware).position = { x: 10, y: 10 };
5959
return model;

packages/sprotty-protocol/src/diagram-server.ts

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ import {
1818
Action, isResponseAction, ResponseAction, RequestModelAction, ComputedBoundsAction, LayoutAction, RequestBoundsAction,
1919
RequestAction, generateRequestId, SetModelAction, UpdateModelAction, RejectAction, isRequestAction
2020
} from './actions';
21+
import { DiagramServices, DiagramState, IDiagramGenerator, IModelLayoutEngine } from './diagram-services';
2122
import { SModelRoot } from './model';
2223
import { Deferred } from './utils/async';
23-
import { JsonMap } from './utils/json';
24-
import { applyBounds, cloneModel, SModelIndex } from './utils/model-utils';
24+
import { applyBounds, cloneModel } from './utils/model-utils';
2525

2626
/**
2727
* An instance of this class is responsible for handling a single diagram client. It holds the current
2828
* state of the diagram and manages communication with the client via actions.
2929
*/
3030
export class DiagramServer {
3131

32-
protected readonly state: DiagramState & {
32+
readonly state: DiagramState & {
3333
lastSubmittedModelType?: string
3434
} = {
3535
currentRoot: {
@@ -38,15 +38,18 @@ export class DiagramServer {
3838
},
3939
revision: 0
4040
};
41+
readonly dispatch: <A extends Action>(action: A) => Promise<void>;
42+
43+
protected readonly diagramGenerator: IDiagramGenerator;
44+
protected readonly layoutEngine?: IModelLayoutEngine;
4145
protected readonly requests = new Map<string, Deferred<ResponseAction>>();
4246
protected readonly handlers = new Map<string, ServerActionHandler[]>();
43-
readonly dispatch: <A extends Action>(action: A) => Promise<void>;
44-
readonly services: DiagramServices;
4547

4648
constructor(dispatch: <A extends Action>(action: A) => Promise<void>,
4749
services: DiagramServices) {
4850
this.dispatch = dispatch;
49-
this.services = services;
51+
this.diagramGenerator = services.DiagramGenerator;
52+
this.layoutEngine = services.ModelLayoutEngine;
5053
}
5154

5255
/**
@@ -201,7 +204,7 @@ export class DiagramServer {
201204
protected async handleRequestModel(action: RequestModelAction): Promise<void> {
202205
this.state.options = action.options;
203206
try {
204-
const newRoot = await this.services.diagramGenerator.generate({
207+
const newRoot = await this.diagramGenerator.generate({
205208
options: this.state.options ?? {},
206209
state: this.state
207210
});
@@ -242,8 +245,8 @@ export class DiagramServer {
242245
if (newRoot.revision !== this.state.revision) {
243246
return;
244247
}
245-
if (this.needsServerLayout) {
246-
newRoot = await this.services.layoutEngine.layout(newRoot);
248+
if (this.needsServerLayout && this.layoutEngine) {
249+
newRoot = await this.layoutEngine.layout(newRoot);
247250
}
248251
const modelType = newRoot.type;
249252
if (cause && cause.kind === RequestModelAction.KIND) {
@@ -266,37 +269,19 @@ export class DiagramServer {
266269
return Promise.resolve();
267270
}
268271

269-
protected handleLayout(action: LayoutAction): Promise<void> {
272+
protected async handleLayout(action: LayoutAction): Promise<void> {
273+
if (!this.layoutEngine) {
274+
return;
275+
}
270276
if (!this.needsServerLayout) {
271-
return Promise.resolve();
277+
let newRoot = cloneModel(this.state.currentRoot);
278+
newRoot = await this.layoutEngine.layout(newRoot);
279+
newRoot.revision = ++this.state.revision;
280+
this.state.currentRoot = newRoot;
272281
}
273-
const newRoot = cloneModel(this.state.currentRoot);
274-
newRoot.revision = ++this.state.revision;
275-
this.state.currentRoot = newRoot;
276-
return this.doSubmitModel(newRoot, true, action);
282+
await this.doSubmitModel(this.state.currentRoot, true, action);
277283
}
278284

279285
}
280286

281-
export type DiagramOptions = JsonMap;
282-
283-
export interface IModelLayoutEngine {
284-
layout(model: SModelRoot, index?: SModelIndex): SModelRoot | Promise<SModelRoot>;
285-
}
286-
287-
export interface IDiagramGenerator {
288-
generate(args: { options: DiagramOptions, state: DiagramState }): SModelRoot | Promise<SModelRoot>
289-
}
290-
291-
export interface DiagramServices {
292-
readonly diagramGenerator: IDiagramGenerator
293-
readonly layoutEngine: IModelLayoutEngine;
294-
}
295-
296-
export interface DiagramState {
297-
options?: DiagramOptions;
298-
currentRoot: SModelRoot;
299-
revision: number;
300-
}
301-
302287
export type ServerActionHandler<A extends Action = Action> = (action: A, state: DiagramState, server: DiagramServer) => Promise<void>;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/********************************************************************************
2+
* Copyright (c) 2021 TypeFox and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { SModelRoot } from './model';
18+
import { JsonMap } from './utils/json';
19+
import { SModelIndex } from './utils/model-utils';
20+
21+
export type DiagramOptions = JsonMap;
22+
23+
/**
24+
* The current state captured by a `DiagramServer`.
25+
*/
26+
export interface DiagramState {
27+
options?: DiagramOptions;
28+
currentRoot: SModelRoot;
29+
revision: number;
30+
}
31+
32+
/**
33+
* The set of services required by a `DiagramServer`.
34+
*/
35+
export interface DiagramServices {
36+
readonly DiagramGenerator: IDiagramGenerator
37+
readonly ModelLayoutEngine?: IModelLayoutEngine
38+
}
39+
40+
/**
41+
* A diagram generator is responsible for creating a diagram model from some source.
42+
* This process is controlled by the `DiagramOptions`, which for example may contain
43+
* a URI to the source document from which the diagram shall be created.
44+
*/
45+
export interface IDiagramGenerator {
46+
generate(args: GeneratorArguments): SModelRoot | Promise<SModelRoot>
47+
}
48+
49+
export interface GeneratorArguments {
50+
options: DiagramOptions
51+
state: DiagramState
52+
}
53+
54+
/**
55+
* This service is responsible for the "macro layout" of a model, that is the positioning
56+
* and sizing of the main structural elements of a model. In a graph, macro layout affects
57+
* positions of nodes and routings of edges, but not necessarily the layout of labels and
58+
* compartments inside a node, which are often arranged on the client side ("micro layout").
59+
*/
60+
export interface IModelLayoutEngine {
61+
layout(model: SModelRoot, index?: SModelIndex): SModelRoot | Promise<SModelRoot>;
62+
}

0 commit comments

Comments
 (0)