Skip to content

Commit e058bb2

Browse files
committed
#10: Allow limits on zooming and panning
1 parent 04037bf commit e058bb2

File tree

14 files changed

+369
-127
lines changed

14 files changed

+369
-127
lines changed

.vscode/launch.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
"type": "node",
77
"request": "launch",
88
"cwd": "${workspaceRoot}",
9-
"program": "${workspaceRoot}/node_modules/.bin/_mocha",
9+
"program": "${workspaceRoot}/node_modules/.bin/ts-mocha",
1010
"args": [
1111
"${file}",
12-
"--no-timeouts",
13-
"--config",
14-
"${workspaceRoot}/configs/.mocharc.json"
12+
"--no-timeouts"
1513
],
1614
"env": {
1715
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"editor.tabSize": 4
1111
},
1212
"typescript.tsdk": "node_modules/typescript/lib",
13+
"typescript.enablePromptUseWorkspaceTsdk": true,
1314
"files.trimTrailingWhitespace": true,
1415
}

examples/svg/src/di.config.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2017-2020 TypeFox and others.
2+
* Copyright (c) 2017-2023 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
@@ -17,10 +17,10 @@
1717
import { Container, ContainerModule } from 'inversify';
1818
import {
1919
TYPES, ConsoleLogger, LogLevel, loadDefaultModules, LocalModelSource, PreRenderedView,
20-
ProjectedViewportView, ViewportRootElement, ShapedPreRenderedElement, configureModelElement,
21-
ForeignObjectElement, ForeignObjectView, RectangularNode, RectangularNodeView, moveFeature,
20+
ProjectedViewportView, ViewportRootElement, ShapedPreRenderedElementImpl, configureModelElement,
21+
ForeignObjectElementImpl, ForeignObjectView, RectangularNode, RectangularNodeView, moveFeature,
2222
selectFeature, EditableLabel, editLabelFeature, WithEditableLabel, withEditLabelFeature,
23-
isEditableLabel
23+
isEditableLabel, configureViewerOptions
2424
} from 'sprotty';
2525

2626
export default () => {
@@ -33,16 +33,23 @@ export default () => {
3333
rebind(TYPES.LogLevel).toConstantValue(LogLevel.log);
3434
bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope();
3535
const context = { bind, unbind, isBound, rebind };
36+
3637
configureModelElement(context, 'svg', ViewportRootElement, ProjectedViewportView);
37-
configureModelElement(context, 'pre-rendered', ShapedPreRenderedElement, PreRenderedView);
38-
configureModelElement(context, 'foreign-object', ForeignObjectElement, ForeignObjectView);
38+
configureModelElement(context, 'pre-rendered', ShapedPreRenderedElementImpl, PreRenderedView);
39+
configureModelElement(context, 'foreign-object', ForeignObjectElementImpl, ForeignObjectView);
3940
configureModelElement(context, 'node', RectangleWithEditableLabel, RectangularNodeView, {
4041
enable: [withEditLabelFeature]
4142
});
4243
configureModelElement(context, 'child-foreign-object', EditableForeignObjectElement, ForeignObjectView, {
4344
disable: [moveFeature, selectFeature], // disable move/select as we want the parent node to react to select/move
4445
enable: [editLabelFeature] // enable editing -- see also EditableForeignObjectElement below
4546
});
47+
48+
configureViewerOptions(context, {
49+
zoomLimits: { min: 0.4, max: 5 },
50+
horizontalScrollLimits: { min: -500, max: 2000 },
51+
verticalScrollLimits: { min: -500, max: 1500 }
52+
});
4653
});
4754

4855
const container = new Container();
@@ -60,7 +67,7 @@ export class RectangleWithEditableLabel extends RectangularNode implements WithE
6067
}
6168
}
6269

63-
export class EditableForeignObjectElement extends ForeignObjectElement implements EditableLabel {
70+
export class EditableForeignObjectElement extends ForeignObjectElementImpl implements EditableLabel {
6471
readonly isMultiLine = true;
6572
get editControlDimension() { return { width: this.bounds.width, height: this.bounds.height }; }
6673

packages/sprotty-protocol/src/actions.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,42 @@ export namespace SelectAllAction {
376376
}
377377
}
378378

379+
/**
380+
* Request action for retrieving the current selection.
381+
*/
382+
export interface GetSelectionAction extends RequestAction<SelectionResult> {
383+
kind: typeof GetSelectionAction.KIND
384+
}
385+
export namespace GetSelectionAction {
386+
export const KIND = 'getSelection';
387+
388+
export function create(): GetSelectionAction {
389+
return {
390+
kind: KIND,
391+
requestId: generateRequestId()
392+
};
393+
}
394+
}
395+
396+
/**
397+
* Result for a `GetSelectionAction`.
398+
*/
399+
export interface SelectionResult extends ResponseAction {
400+
kind: typeof SelectionResult.KIND
401+
selectedElementsIDs: string[]
402+
}
403+
export namespace SelectionResult {
404+
export const KIND = 'selectionResult';
405+
406+
export function create(selectedElementsIDs: string[], requestId: string): SelectionResult {
407+
return {
408+
kind: KIND,
409+
selectedElementsIDs,
410+
responseId: requestId
411+
};
412+
}
413+
}
414+
379415
/**
380416
* Sent from the client to the model source to recalculate a diagram when elements
381417
* are collapsed/expanded by the client.
@@ -530,6 +566,44 @@ export namespace SetViewportAction {
530566
}
531567
}
532568

569+
/**
570+
* Request action for retrieving the current viewport and canvas bounds.
571+
*/
572+
export interface GetViewportAction extends RequestAction<ViewportResult> {
573+
kind: typeof GetViewportAction.KIND;
574+
}
575+
export namespace GetViewportAction {
576+
export const KIND = 'getViewport';
577+
578+
export function create(): GetViewportAction {
579+
return {
580+
kind: KIND,
581+
requestId: generateRequestId()
582+
};
583+
}
584+
}
585+
586+
/**
587+
* Response to a `GetViewportAction`.
588+
*/
589+
export interface ViewportResult extends ResponseAction {
590+
kind: typeof ViewportResult.KIND;
591+
viewport: Viewport
592+
canvasBounds: Bounds
593+
}
594+
export namespace ViewportResult {
595+
export const KIND = 'viewportResult';
596+
597+
export function create(viewport: Viewport, canvasBounds: Bounds, requestId: string): ViewportResult {
598+
return {
599+
kind: KIND,
600+
viewport,
601+
canvasBounds,
602+
responseId: requestId
603+
};
604+
}
605+
}
606+
533607
/**
534608
* Action to render the selected elements in front of others by manipulating the z-order.
535609
*/

packages/sprotty/src/base/views/viewer-options.ts

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

17-
import { Container, interfaces } from "inversify";
18-
import { safeAssign } from "sprotty-protocol/lib/utils/object";
19-
import { TYPES } from "../types";
17+
import { Container, interfaces } from 'inversify';
18+
import { safeAssign } from 'sprotty-protocol/lib/utils/object';
19+
import { Limits } from '../../utils/geometry';
20+
import { TYPES } from '../types';
2021

2122
export interface ViewerOptions {
23+
/** ID of the HTML element into which the visible diagram is rendered. */
2224
baseDiv: string
25+
/** CSS class added to the base element of the visible diagram. */
2326
baseClass: string
27+
/** ID of the HTML element into which the hidden diagram is rendered. */
2428
hiddenDiv: string
29+
/** CSS class added to the base element of the hidden rendering. */
2530
hiddenClass: string
31+
/** ID of the HTML element into which hover popup boxes are rendered. */
2632
popupDiv: string
33+
/** CSS class added to the base element of popup boxes. */
2734
popupClass: string
35+
/** CSS class added to popup boxes when they are closed. */
2836
popupClosedClass: string
37+
/** Whether client layouts need to be computed by Sprotty. This activates a hidden rendering cycle. */
2938
needsClientLayout: boolean
39+
/** Whether the model source needs to invoke a layout engine after a model update. */
3040
needsServerLayout: boolean
41+
/** Delay for opening a popup box after mouse hovering an element. */
3142
popupOpenDelay: number
43+
/** Delay for closing a popup box after leaving the corresponding element. */
3244
popupCloseDelay: number
45+
/** Minimum (zoom out) and maximum (zoom in) values for the zoom factor. */
46+
zoomLimits: Limits
47+
/** Minimum and maximum values for the horizontal scroll position. */
48+
horizontalScrollLimits: Limits
49+
/** Minimum and maximum values for the vertical scroll position. */
50+
verticalScrollLimits: Limits
3351
}
3452

35-
export const defaultViewerOptions = () => (<ViewerOptions>{
53+
export const defaultViewerOptions: () => ViewerOptions = () => ({
3654
baseDiv: 'sprotty',
3755
baseClass: 'sprotty',
3856
hiddenDiv: 'sprotty-hidden',
@@ -43,7 +61,10 @@ export const defaultViewerOptions = () => (<ViewerOptions>{
4361
needsClientLayout: true,
4462
needsServerLayout: false,
4563
popupOpenDelay: 1000,
46-
popupCloseDelay: 300
64+
popupCloseDelay: 300,
65+
zoomLimits: { min: 0.01, max: 10 },
66+
horizontalScrollLimits: { min: -100_000, max: 100_000 },
67+
verticalScrollLimits: { min: -100_000, max: 100_000 }
4768
});
4869

4970
/**

packages/sprotty/src/features/select/select.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class SelectAllAction implements Action, ProtocolSelectAllActon {
7474

7575
/**
7676
* Request action for retrieving the current selection.
77+
* @deprecated Use the declaration from `sprotty-protocol` instead.
7778
*/
7879
export interface GetSelectionAction extends RequestAction<SelectionResult> {
7980
kind: typeof GetSelectionAction.KIND
@@ -89,6 +90,9 @@ export namespace GetSelectionAction {
8990
}
9091
}
9192

93+
/**
94+
* @deprecated Use the declaration from `sprotty-protocol` instead.
95+
*/
9296
export interface SelectionResult extends ResponseAction {
9397
kind: typeof SelectionResult.KIND
9498
selectedElementsIDs: string[]

packages/sprotty/src/features/viewport/center-fit.ts

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

17-
import { Action, CenterAction as ProtocolCenterAction, FitToScreenAction as ProtocolFitToScreenAction} from "sprotty-protocol/lib/actions";
18-
import { Viewport } from "sprotty-protocol/lib/model";
19-
import { Bounds, Dimension } from "sprotty-protocol/lib/utils/geometry";
20-
import { matchesKeystroke } from "../../utils/keyboard";
17+
import { Action, CenterAction as ProtocolCenterAction, FitToScreenAction as ProtocolFitToScreenAction} from 'sprotty-protocol/lib/actions';
18+
import { Viewport } from 'sprotty-protocol/lib/model';
19+
import { almostEquals, Bounds, Dimension } from 'sprotty-protocol/lib/utils/geometry';
20+
import { matchesKeystroke } from '../../utils/keyboard';
2121
import { SChildElementImpl } from '../../base/model/smodel';
22-
import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command";
23-
import { SModelElementImpl, SModelRootImpl } from "../../base/model/smodel";
24-
import { KeyListener } from "../../base/views/key-tool";
25-
import { isBoundsAware } from "../bounds/model";
26-
import { isSelectable } from "../select/model";
27-
import { ViewportAnimation } from "./viewport";
28-
import { isViewport } from "./model";
29-
import { injectable, inject } from "inversify";
30-
import { TYPES } from "../../base/types";
22+
import { Command, CommandExecutionContext, CommandReturn } from '../../base/commands/command';
23+
import { SModelElementImpl, SModelRootImpl } from '../../base/model/smodel';
24+
import { KeyListener } from '../../base/views/key-tool';
25+
import { isBoundsAware } from '../bounds/model';
26+
import { isSelectable } from '../select/model';
27+
import { ViewportAnimation } from './viewport';
28+
import { isViewport, limitViewport } from './model';
29+
import { injectable, inject } from 'inversify';
30+
import { TYPES } from '../../base/types';
31+
import { ViewerOptions } from '../../base/views/viewer-options';
3132

3233
/**
3334
* Triggered when the user requests the viewer to center on the current model. The resulting
@@ -71,47 +72,51 @@ export class FitToScreenAction implements Action, ProtocolFitToScreenAction {
7172
@injectable()
7273
export abstract class BoundsAwareViewportCommand extends Command {
7374

75+
@inject(TYPES.ViewerOptions) protected viewerOptions: ViewerOptions;
7476
oldViewport: Viewport;
7577
newViewport?: Viewport;
7678

7779
constructor(protected readonly animate: boolean) {
7880
super();
7981
}
8082

81-
protected initialize(model: SModelRootImpl) {
82-
if (isViewport(model)) {
83-
this.oldViewport = {
84-
scroll: model.scroll,
85-
zoom: model.zoom
86-
};
87-
const allBounds: Bounds[] = [];
88-
this.getElementIds().forEach(
89-
id => {
90-
const element = model.index.getById(id);
91-
if (element && isBoundsAware(element))
92-
allBounds.push(this.boundsInViewport(element, element.bounds, model));
93-
}
94-
);
95-
if (allBounds.length === 0) {
96-
model.index.all().forEach(
97-
element => {
98-
if (isSelectable(element) && element.selected && isBoundsAware(element))
99-
allBounds.push(this.boundsInViewport(element, element.bounds, model));
100-
}
101-
);
102-
}
103-
if (allBounds.length === 0) {
104-
model.index.all().forEach(
105-
element => {
106-
if (isBoundsAware(element))
107-
allBounds.push(this.boundsInViewport(element, element.bounds, model));
108-
}
109-
);
83+
protected initialize(model: SModelRootImpl): void {
84+
if (!isViewport(model)) {
85+
return;
86+
}
87+
this.oldViewport = {
88+
scroll: model.scroll,
89+
zoom: model.zoom
90+
};
91+
const allBounds: Bounds[] = [];
92+
this.getElementIds().forEach(id => {
93+
const element = model.index.getById(id);
94+
if (element && isBoundsAware(element)) {
95+
allBounds.push(this.boundsInViewport(element, element.bounds, model));
11096
}
111-
if (allBounds.length !== 0) {
112-
const bounds = allBounds.reduce((b0, b1) => Bounds.combine(b0, b1));
113-
if (Dimension.isValid(bounds))
114-
this.newViewport = this.getNewViewport(bounds, model);
97+
});
98+
if (allBounds.length === 0) {
99+
model.index.all().forEach(element => {
100+
if (isSelectable(element) && element.selected && isBoundsAware(element)) {
101+
allBounds.push(this.boundsInViewport(element, element.bounds, model));
102+
}
103+
});
104+
}
105+
if (allBounds.length === 0) {
106+
model.index.all().forEach(element => {
107+
if (isBoundsAware(element)) {
108+
allBounds.push(this.boundsInViewport(element, element.bounds, model));
109+
}
110+
});
111+
}
112+
if (allBounds.length !== 0) {
113+
const bounds = allBounds.reduce((b0, b1) => Bounds.combine(b0, b1));
114+
if (Dimension.isValid(bounds)) {
115+
const newViewport = this.getNewViewport(bounds, model);
116+
if (newViewport) {
117+
const { zoomLimits, horizontalScrollLimits, verticalScrollLimits } = this.viewerOptions;
118+
this.newViewport = limitViewport(newViewport, model.canvasBounds, horizontalScrollLimits, verticalScrollLimits, zoomLimits);
119+
}
115120
}
116121
}
117122
}
@@ -159,7 +164,7 @@ export abstract class BoundsAwareViewportCommand extends Command {
159164
}
160165

161166
protected equal(vp1: Viewport, vp2: Viewport): boolean {
162-
return vp1.zoom === vp2.zoom && vp1.scroll.x === vp2.scroll.x && vp1.scroll.y === vp2.scroll.y;
167+
return almostEquals(vp1.zoom, vp2.zoom) && almostEquals(vp1.scroll.x, vp2.scroll.x) && almostEquals(vp1.scroll.y, vp2.scroll.y);
163168
}
164169
}
165170

0 commit comments

Comments
 (0)