sprotty
Version:
A next-gen framework for graphical views
289 lines (270 loc) • 12.9 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2022 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
* 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 { injectable, inject, optional } from 'inversify';
import { UpdateModelAction } from 'sprotty-protocol/lib/actions';
import { Fadeable } from 'sprotty-protocol/lib/model';
import { almostEquals, Dimension } from 'sprotty-protocol/lib/utils/geometry';
import { Animation, CompoundAnimation } from '../../base/animations/animation';
import { CommandExecutionContext, CommandReturn, Command } from '../../base/commands/command';
import { FadeAnimation, ResolvedElementFade } from '../fade/fade';
import { SModelRootImpl, SChildElementImpl, SModelElementImpl, SParentElementImpl } from '../../base/model/smodel';
import { MoveAnimation, ResolvedElementMove, MorphEdgesAnimation } from '../move/move';
import { isFadeable } from '../fade/model';
import { isLocateable } from '../move/model';
import { isSizeable } from '../bounds/model';
import { ViewportRootElementImpl } from '../viewport/viewport-root';
import { isSelectable } from '../select/model';
import { MatchResult, ModelMatcher, Match, forEachMatch } from './model-matching';
import { ResolvedElementResize, ResizeAnimation } from '../bounds/resize';
import { TYPES } from '../../base/types';
import { isViewport } from '../viewport/model';
import { EdgeRouterRegistry, EdgeSnapshot, EdgeMemento } from '../routing/routing';
import { SRoutableElementImpl } from '../routing/model';
import { containsSome } from '../../base/model/smodel-utils';
export interface UpdateAnimationData {
fades: ResolvedElementFade[]
moves?: ResolvedElementMove[]
resizes?: ResolvedElementResize[]
edgeMementi?: EdgeMemento[]
}
export class UpdateModelCommand extends Command {
static readonly KIND = UpdateModelAction.KIND;
oldRoot: SModelRootImpl;
newRoot: SModelRootImpl;
edgeRouterRegistry?: EdgeRouterRegistry;
constructor( protected readonly action: UpdateModelAction) {
super();
}
execute(context: CommandExecutionContext): CommandReturn {
let newRoot: SModelRootImpl;
if (this.action.newRoot !== undefined) {
newRoot = context.modelFactory.createRoot(this.action.newRoot);
} else {
newRoot = context.modelFactory.createRoot(context.root);
if (this.action.matches !== undefined)
this.applyMatches(newRoot, this.action.matches, context);
}
this.oldRoot = context.root;
this.newRoot = newRoot;
return this.performUpdate(this.oldRoot, this.newRoot, context);
}
protected performUpdate(oldRoot: SModelRootImpl, newRoot: SModelRootImpl, context: CommandExecutionContext): CommandReturn {
if ((this.action.animate === undefined || this.action.animate) && oldRoot.id === newRoot.id) {
let matchResult: MatchResult;
if (this.action.matches === undefined) {
const matcher = new ModelMatcher();
matchResult = matcher.match(oldRoot, newRoot);
} else {
matchResult = this.convertToMatchResult(this.action.matches, oldRoot, newRoot);
}
const animationOrRoot = this.computeAnimation(newRoot, matchResult, context);
if (animationOrRoot instanceof Animation)
return animationOrRoot.start();
else
return animationOrRoot;
} else {
if (oldRoot.type === newRoot.type && Dimension.isValid(oldRoot.canvasBounds))
newRoot.canvasBounds = oldRoot.canvasBounds;
if (isViewport(oldRoot) && isViewport(newRoot)) {
newRoot.zoom = oldRoot.zoom;
newRoot.scroll = oldRoot.scroll;
}
return newRoot;
}
}
protected applyMatches(root: SModelRootImpl, matches: Match[], context: CommandExecutionContext): void {
const index = root.index;
for (const match of matches) {
if (match.left !== undefined) {
const element = index.getById(match.left.id);
if (element instanceof SChildElementImpl)
element.parent.remove(element);
}
}
for (const match of matches) {
if (match.right !== undefined) {
const element = context.modelFactory.createElement(match.right);
let parent: SModelElementImpl | undefined;
if (match.rightParentId !== undefined)
parent = index.getById(match.rightParentId);
if (parent instanceof SParentElementImpl)
parent.add(element);
else
root.add(element);
}
}
}
protected convertToMatchResult(matches: Match[], leftRoot: SModelRootImpl, rightRoot: SModelRootImpl): MatchResult {
const result: MatchResult = {};
for (const match of matches) {
const converted: Match = {};
let id: string | undefined = undefined;
if (match.left !== undefined) {
id = match.left.id;
converted.left = leftRoot.index.getById(id);
converted.leftParentId = match.leftParentId;
}
if (match.right !== undefined) {
id = match.right.id;
converted.right = rightRoot.index.getById(id);
converted.rightParentId = match.rightParentId;
}
if (id !== undefined)
result[id] = converted;
}
return result;
}
protected computeAnimation(newRoot: SModelRootImpl, matchResult: MatchResult, context: CommandExecutionContext): SModelRootImpl | Animation {
const animationData: UpdateAnimationData = {
fades: [] as ResolvedElementFade[]
};
forEachMatch(matchResult, (id, match) => {
if (match.left !== undefined && match.right !== undefined) {
// The element is still there, but may have been moved
this.updateElement(match.left as SModelElementImpl, match.right as SModelElementImpl, animationData);
} else if (match.right !== undefined) {
// An element has been added
const right = match.right as SModelElementImpl;
if (isFadeable(right)) {
right.opacity = 0;
animationData.fades.push({
element: right,
type: 'in'
});
}
} else if (match.left instanceof SChildElementImpl) {
// An element has been removed
const left = match.left;
if (isFadeable(left) && match.leftParentId !== undefined) {
if (!containsSome(newRoot, left)) {
const parent = newRoot.index.getById(match.leftParentId);
if (parent instanceof SParentElementImpl) {
const leftCopy = context.modelFactory.createElement(left) as SChildElementImpl & Fadeable;
parent.add(leftCopy);
animationData.fades.push({
element: leftCopy,
type: 'out'
});
}
}
}
}
});
const animations = this.createAnimations(animationData, newRoot, context);
if (animations.length >= 2) {
return new CompoundAnimation(newRoot, context, animations);
} else if (animations.length === 1) {
return animations[0];
} else {
return newRoot;
}
}
protected updateElement(left: SModelElementImpl, right: SModelElementImpl, animationData: UpdateAnimationData): void {
if (isLocateable(left) && isLocateable(right)) {
const leftPos = left.position;
const rightPos = right.position;
if (!almostEquals(leftPos.x, rightPos.x) || !almostEquals(leftPos.y, rightPos.y)) {
if (animationData.moves === undefined)
animationData.moves = [];
animationData.moves.push({
element: right,
fromPosition: leftPos,
toPosition: rightPos
});
right.position = leftPos;
}
}
if (isSizeable(left) && isSizeable(right)) {
if (!Dimension.isValid(right.bounds)) {
right.bounds = {
x: right.bounds.x,
y: right.bounds.y,
width: left.bounds.width,
height: left.bounds.height
};
} else if (!almostEquals(left.bounds.width, right.bounds.width)
|| !almostEquals(left.bounds.height, right.bounds.height)) {
if (animationData.resizes === undefined)
animationData.resizes = [];
animationData.resizes.push({
element: right,
fromDimension: {
width: left.bounds.width,
height: left.bounds.height,
},
toDimension: {
width: right.bounds.width,
height: right.bounds.height,
}
});
}
}
if (left instanceof SRoutableElementImpl && right instanceof SRoutableElementImpl && this.edgeRouterRegistry) {
if (animationData.edgeMementi === undefined)
animationData.edgeMementi = [];
animationData.edgeMementi.push({
edge: right,
before: this.takeSnapshot(left),
after: this.takeSnapshot(right),
});
}
if (isSelectable(left) && isSelectable(right)) {
right.selected = left.selected;
}
if (left instanceof SModelRootImpl && right instanceof SModelRootImpl) {
right.canvasBounds = left.canvasBounds;
}
if (left instanceof ViewportRootElementImpl && right instanceof ViewportRootElementImpl) {
right.scroll = left.scroll;
right.zoom = left.zoom;
}
}
protected takeSnapshot(edge: SRoutableElementImpl): EdgeSnapshot {
const router = this.edgeRouterRegistry!.get(edge.routerKind);
return router.takeSnapshot(edge);
}
protected createAnimations(data: UpdateAnimationData, root: SModelRootImpl, context: CommandExecutionContext): Animation[] {
const animations: Animation[] = [];
if (data.fades.length > 0) {
animations.push(new FadeAnimation(root, data.fades, context, true));
}
if (data.moves !== undefined && data.moves.length > 0) {
const movesMap: Map<string, ResolvedElementMove> = new Map;
for (const move of data.moves) {
movesMap.set(move.element.id, move);
}
animations.push(new MoveAnimation(root, movesMap, context, false));
}
if (data.resizes !== undefined && data.resizes.length > 0) {
const resizesMap: Map<string, ResolvedElementResize> = new Map;
for (const resize of data.resizes) {
resizesMap.set(resize.element.id, resize);
}
animations.push(new ResizeAnimation(root, resizesMap, context, false));
}
if (data.edgeMementi !== undefined && data.edgeMementi.length > 0) {
animations.push(new MorphEdgesAnimation(root, data.edgeMementi, context, false));
}
return animations;
}
undo(context: CommandExecutionContext): CommandReturn {
return this.performUpdate(this.newRoot, this.oldRoot, context);
}
redo(context: CommandExecutionContext): CommandReturn {
return this.performUpdate(this.oldRoot, this.newRoot, context);
}
}