Skip to content

Commit

Permalink
#10: Allow limits on zooming and panning
Browse files Browse the repository at this point in the history
  • Loading branch information
spoenemann committed May 26, 2023
1 parent 04037bf commit e058bb2
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 127 deletions.
6 changes: 2 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/node_modules/.bin/_mocha",
"program": "${workspaceRoot}/node_modules/.bin/ts-mocha",
"args": [
"${file}",
"--no-timeouts",
"--config",
"${workspaceRoot}/configs/.mocharc.json"
"--no-timeouts"
],
"env": {
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"editor.tabSize": 4
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.trimTrailingWhitespace": true,
}
21 changes: 14 additions & 7 deletions examples/svg/src/di.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2017-2020 TypeFox and others.
* Copyright (c) 2017-2023 TypeFox 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
Expand All @@ -17,10 +17,10 @@
import { Container, ContainerModule } from 'inversify';
import {
TYPES, ConsoleLogger, LogLevel, loadDefaultModules, LocalModelSource, PreRenderedView,
ProjectedViewportView, ViewportRootElement, ShapedPreRenderedElement, configureModelElement,
ForeignObjectElement, ForeignObjectView, RectangularNode, RectangularNodeView, moveFeature,
ProjectedViewportView, ViewportRootElement, ShapedPreRenderedElementImpl, configureModelElement,
ForeignObjectElementImpl, ForeignObjectView, RectangularNode, RectangularNodeView, moveFeature,
selectFeature, EditableLabel, editLabelFeature, WithEditableLabel, withEditLabelFeature,
isEditableLabel
isEditableLabel, configureViewerOptions
} from 'sprotty';

export default () => {
Expand All @@ -33,16 +33,23 @@ export default () => {
rebind(TYPES.LogLevel).toConstantValue(LogLevel.log);
bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope();
const context = { bind, unbind, isBound, rebind };

configureModelElement(context, 'svg', ViewportRootElement, ProjectedViewportView);
configureModelElement(context, 'pre-rendered', ShapedPreRenderedElement, PreRenderedView);
configureModelElement(context, 'foreign-object', ForeignObjectElement, ForeignObjectView);
configureModelElement(context, 'pre-rendered', ShapedPreRenderedElementImpl, PreRenderedView);
configureModelElement(context, 'foreign-object', ForeignObjectElementImpl, ForeignObjectView);
configureModelElement(context, 'node', RectangleWithEditableLabel, RectangularNodeView, {
enable: [withEditLabelFeature]
});
configureModelElement(context, 'child-foreign-object', EditableForeignObjectElement, ForeignObjectView, {
disable: [moveFeature, selectFeature], // disable move/select as we want the parent node to react to select/move
enable: [editLabelFeature] // enable editing -- see also EditableForeignObjectElement below
});

configureViewerOptions(context, {
zoomLimits: { min: 0.4, max: 5 },
horizontalScrollLimits: { min: -500, max: 2000 },
verticalScrollLimits: { min: -500, max: 1500 }
});
});

const container = new Container();
Expand All @@ -60,7 +67,7 @@ export class RectangleWithEditableLabel extends RectangularNode implements WithE
}
}

export class EditableForeignObjectElement extends ForeignObjectElement implements EditableLabel {
export class EditableForeignObjectElement extends ForeignObjectElementImpl implements EditableLabel {
readonly isMultiLine = true;
get editControlDimension() { return { width: this.bounds.width, height: this.bounds.height }; }

Expand Down
74 changes: 74 additions & 0 deletions packages/sprotty-protocol/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,42 @@ export namespace SelectAllAction {
}
}

/**
* Request action for retrieving the current selection.
*/
export interface GetSelectionAction extends RequestAction<SelectionResult> {
kind: typeof GetSelectionAction.KIND
}
export namespace GetSelectionAction {
export const KIND = 'getSelection';

export function create(): GetSelectionAction {
return {
kind: KIND,
requestId: generateRequestId()
};
}
}

/**
* Result for a `GetSelectionAction`.
*/
export interface SelectionResult extends ResponseAction {
kind: typeof SelectionResult.KIND
selectedElementsIDs: string[]
}
export namespace SelectionResult {
export const KIND = 'selectionResult';

export function create(selectedElementsIDs: string[], requestId: string): SelectionResult {
return {
kind: KIND,
selectedElementsIDs,
responseId: requestId
};
}
}

/**
* Sent from the client to the model source to recalculate a diagram when elements
* are collapsed/expanded by the client.
Expand Down Expand Up @@ -530,6 +566,44 @@ export namespace SetViewportAction {
}
}

/**
* Request action for retrieving the current viewport and canvas bounds.
*/
export interface GetViewportAction extends RequestAction<ViewportResult> {
kind: typeof GetViewportAction.KIND;
}
export namespace GetViewportAction {
export const KIND = 'getViewport';

export function create(): GetViewportAction {
return {
kind: KIND,
requestId: generateRequestId()
};
}
}

/**
* Response to a `GetViewportAction`.
*/
export interface ViewportResult extends ResponseAction {
kind: typeof ViewportResult.KIND;
viewport: Viewport
canvasBounds: Bounds
}
export namespace ViewportResult {
export const KIND = 'viewportResult';

export function create(viewport: Viewport, canvasBounds: Bounds, requestId: string): ViewportResult {
return {
kind: KIND,
viewport,
canvasBounds,
responseId: requestId
};
}
}

/**
* Action to render the selected elements in front of others by manipulating the z-order.
*/
Expand Down
33 changes: 27 additions & 6 deletions packages/sprotty/src/base/views/viewer-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2017-2021 TypeFox and others.
* Copyright (c) 2017-2023 TypeFox 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
Expand All @@ -14,25 +14,43 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

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

export interface ViewerOptions {
/** ID of the HTML element into which the visible diagram is rendered. */
baseDiv: string
/** CSS class added to the base element of the visible diagram. */
baseClass: string
/** ID of the HTML element into which the hidden diagram is rendered. */
hiddenDiv: string
/** CSS class added to the base element of the hidden rendering. */
hiddenClass: string
/** ID of the HTML element into which hover popup boxes are rendered. */
popupDiv: string
/** CSS class added to the base element of popup boxes. */
popupClass: string
/** CSS class added to popup boxes when they are closed. */
popupClosedClass: string
/** Whether client layouts need to be computed by Sprotty. This activates a hidden rendering cycle. */
needsClientLayout: boolean
/** Whether the model source needs to invoke a layout engine after a model update. */
needsServerLayout: boolean
/** Delay for opening a popup box after mouse hovering an element. */
popupOpenDelay: number
/** Delay for closing a popup box after leaving the corresponding element. */
popupCloseDelay: number
/** Minimum (zoom out) and maximum (zoom in) values for the zoom factor. */
zoomLimits: Limits
/** Minimum and maximum values for the horizontal scroll position. */
horizontalScrollLimits: Limits
/** Minimum and maximum values for the vertical scroll position. */
verticalScrollLimits: Limits
}

export const defaultViewerOptions = () => (<ViewerOptions>{
export const defaultViewerOptions: () => ViewerOptions = () => ({
baseDiv: 'sprotty',
baseClass: 'sprotty',
hiddenDiv: 'sprotty-hidden',
Expand All @@ -43,7 +61,10 @@ export const defaultViewerOptions = () => (<ViewerOptions>{
needsClientLayout: true,
needsServerLayout: false,
popupOpenDelay: 1000,
popupCloseDelay: 300
popupCloseDelay: 300,
zoomLimits: { min: 0.01, max: 10 },
horizontalScrollLimits: { min: -100_000, max: 100_000 },
verticalScrollLimits: { min: -100_000, max: 100_000 }
});

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/sprotty/src/features/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class SelectAllAction implements Action, ProtocolSelectAllActon {

/**
* Request action for retrieving the current selection.
* @deprecated Use the declaration from `sprotty-protocol` instead.
*/
export interface GetSelectionAction extends RequestAction<SelectionResult> {
kind: typeof GetSelectionAction.KIND
Expand All @@ -89,6 +90,9 @@ export namespace GetSelectionAction {
}
}

/**
* @deprecated Use the declaration from `sprotty-protocol` instead.
*/
export interface SelectionResult extends ResponseAction {
kind: typeof SelectionResult.KIND
selectedElementsIDs: string[]
Expand Down
101 changes: 53 additions & 48 deletions packages/sprotty/src/features/viewport/center-fit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2017-2018 TypeFox and others.
* Copyright (c) 2017-2023 TypeFox 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
Expand All @@ -14,20 +14,21 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { Action, CenterAction as ProtocolCenterAction, FitToScreenAction as ProtocolFitToScreenAction} from "sprotty-protocol/lib/actions";
import { Viewport } from "sprotty-protocol/lib/model";
import { Bounds, Dimension } from "sprotty-protocol/lib/utils/geometry";
import { matchesKeystroke } from "../../utils/keyboard";
import { Action, CenterAction as ProtocolCenterAction, FitToScreenAction as ProtocolFitToScreenAction} from 'sprotty-protocol/lib/actions';
import { Viewport } from 'sprotty-protocol/lib/model';
import { almostEquals, Bounds, Dimension } from 'sprotty-protocol/lib/utils/geometry';
import { matchesKeystroke } from '../../utils/keyboard';
import { SChildElementImpl } from '../../base/model/smodel';
import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command";
import { SModelElementImpl, SModelRootImpl } from "../../base/model/smodel";
import { KeyListener } from "../../base/views/key-tool";
import { isBoundsAware } from "../bounds/model";
import { isSelectable } from "../select/model";
import { ViewportAnimation } from "./viewport";
import { isViewport } from "./model";
import { injectable, inject } from "inversify";
import { TYPES } from "../../base/types";
import { Command, CommandExecutionContext, CommandReturn } from '../../base/commands/command';
import { SModelElementImpl, SModelRootImpl } from '../../base/model/smodel';
import { KeyListener } from '../../base/views/key-tool';
import { isBoundsAware } from '../bounds/model';
import { isSelectable } from '../select/model';
import { ViewportAnimation } from './viewport';
import { isViewport, limitViewport } from './model';
import { injectable, inject } from 'inversify';
import { TYPES } from '../../base/types';
import { ViewerOptions } from '../../base/views/viewer-options';

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

@inject(TYPES.ViewerOptions) protected viewerOptions: ViewerOptions;
oldViewport: Viewport;
newViewport?: Viewport;

constructor(protected readonly animate: boolean) {
super();
}

protected initialize(model: SModelRootImpl) {
if (isViewport(model)) {
this.oldViewport = {
scroll: model.scroll,
zoom: model.zoom
};
const allBounds: Bounds[] = [];
this.getElementIds().forEach(
id => {
const element = model.index.getById(id);
if (element && isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
);
if (allBounds.length === 0) {
model.index.all().forEach(
element => {
if (isSelectable(element) && element.selected && isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
);
}
if (allBounds.length === 0) {
model.index.all().forEach(
element => {
if (isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
);
protected initialize(model: SModelRootImpl): void {
if (!isViewport(model)) {
return;
}
this.oldViewport = {
scroll: model.scroll,
zoom: model.zoom
};
const allBounds: Bounds[] = [];
this.getElementIds().forEach(id => {
const element = model.index.getById(id);
if (element && isBoundsAware(element)) {
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
if (allBounds.length !== 0) {
const bounds = allBounds.reduce((b0, b1) => Bounds.combine(b0, b1));
if (Dimension.isValid(bounds))
this.newViewport = this.getNewViewport(bounds, model);
});
if (allBounds.length === 0) {
model.index.all().forEach(element => {
if (isSelectable(element) && element.selected && isBoundsAware(element)) {
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
});
}
if (allBounds.length === 0) {
model.index.all().forEach(element => {
if (isBoundsAware(element)) {
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
});
}
if (allBounds.length !== 0) {
const bounds = allBounds.reduce((b0, b1) => Bounds.combine(b0, b1));
if (Dimension.isValid(bounds)) {
const newViewport = this.getNewViewport(bounds, model);
if (newViewport) {
const { zoomLimits, horizontalScrollLimits, verticalScrollLimits } = this.viewerOptions;
this.newViewport = limitViewport(newViewport, model.canvasBounds, horizontalScrollLimits, verticalScrollLimits, zoomLimits);
}
}
}
}
Expand Down Expand Up @@ -159,7 +164,7 @@ export abstract class BoundsAwareViewportCommand extends Command {
}

protected equal(vp1: Viewport, vp2: Viewport): boolean {
return vp1.zoom === vp2.zoom && vp1.scroll.x === vp2.scroll.x && vp1.scroll.y === vp2.scroll.y;
return almostEquals(vp1.zoom, vp2.zoom) && almostEquals(vp1.scroll.x, vp2.scroll.x) && almostEquals(vp1.scroll.y, vp2.scroll.y);
}
}

Expand Down
Loading

0 comments on commit e058bb2

Please sign in to comment.