Comparing version 0.4.0-next.f74c0b42 to 0.4.0
@@ -7,4 +7,4 @@ import { ILogger } from "../../utils/logging"; | ||
export interface IActionDispatcher { | ||
dispatch(action: Action, onExecute?: (action: Action) => void): void; | ||
dispatchAll(actions: Action[]): void; | ||
dispatch(action: Action): Promise<void>; | ||
dispatchAll(actions: Action[]): Promise<void>; | ||
} | ||
@@ -20,12 +20,14 @@ /** | ||
protected syncer: AnimationFrameSyncer; | ||
protected blockUntilActionKind: string | undefined; | ||
protected postponedActions: ActionAndHook[]; | ||
protected blockUntil?: (action: Action) => boolean; | ||
protected postponedActions: PostponedAction[]; | ||
constructor(actionHandlerRegistry: ActionHandlerRegistry, commandStack: ICommandStack, logger: ILogger, syncer: AnimationFrameSyncer); | ||
dispatchAll(actions: Action[]): void; | ||
dispatch(action: Action, onExecute?: (action: Action) => void): void; | ||
protected handleAction(action: Action): void; | ||
dispatchAll(actions: Action[]): Promise<void>; | ||
dispatch(action: Action): Promise<void>; | ||
protected handleAction(action: Action): Promise<void>; | ||
protected handleBlocked(action: Action, predicate: (action: Action) => boolean): Promise<void>; | ||
} | ||
export interface ActionAndHook { | ||
export interface PostponedAction { | ||
action: Action; | ||
onExecute?: (action: Action) => void; | ||
resolve: () => void; | ||
reject: (reason: any) => void; | ||
} |
@@ -40,4 +40,5 @@ "use strict"; | ||
this.postponedActions = []; | ||
this.postponedActions = []; | ||
var initialCommand = new set_model_1.SetModelCommand(new set_model_1.SetModelAction(smodel_factory_1.EMPTY_ROOT)); | ||
this.blockUntilActionKind = initialCommand.blockUntilActionKind; | ||
this.blockUntil = initialCommand.blockUntil; | ||
this.commandStack.execute(initialCommand); | ||
@@ -47,32 +48,16 @@ } | ||
var _this = this; | ||
actions.forEach(function (action) { return _this.dispatch(action); }); | ||
return Promise.all(actions.map(function (action) { return _this.dispatch(action); })); | ||
}; | ||
ActionDispatcher.prototype.dispatch = function (action, onExecute) { | ||
var _this = this; | ||
if (action.kind === this.blockUntilActionKind) { | ||
this.blockUntilActionKind = undefined; | ||
this.handleAction(action); | ||
var actions = this.postponedActions; | ||
this.postponedActions = []; | ||
actions.forEach(function (a) { return _this.dispatch(a.action, a.onExecute); }); | ||
return; | ||
ActionDispatcher.prototype.dispatch = function (action) { | ||
if (this.blockUntil !== undefined) { | ||
return this.handleBlocked(action, this.blockUntil); | ||
} | ||
if (this.blockUntilActionKind !== undefined) { | ||
this.logger.log(this, 'waiting for ' + this.blockUntilActionKind + '. postponing', action); | ||
this.postponedActions.push({ | ||
action: action, | ||
onExecute: onExecute | ||
}); | ||
return; | ||
else if (action.kind === undo_redo_1.UndoAction.KIND) { | ||
return this.commandStack.undo().then(function () { }); | ||
} | ||
if (onExecute !== undefined) | ||
onExecute.call(null, action); | ||
if (action.kind === undo_redo_1.UndoAction.KIND) { | ||
this.commandStack.undo(); | ||
} | ||
else if (action.kind === undo_redo_1.RedoAction.KIND) { | ||
this.commandStack.redo(); | ||
return this.commandStack.redo().then(function () { }); | ||
} | ||
else { | ||
this.handleAction(action); | ||
return this.handleAction(action); | ||
} | ||
@@ -84,17 +69,41 @@ }; | ||
if (handlers.length > 0) { | ||
var promises = []; | ||
for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) { | ||
var handler = handlers_1[_i]; | ||
var result = handler.handle(action); | ||
if (action_1.isAction(result)) | ||
this.dispatch(result); | ||
if (action_1.isAction(result)) { | ||
promises.push(this.dispatch(result)); | ||
} | ||
else if (result !== undefined) { | ||
this.commandStack.execute(result); | ||
this.blockUntilActionKind = result.blockUntilActionKind; | ||
promises.push(this.commandStack.execute(result)); | ||
this.blockUntil = result.blockUntil; | ||
} | ||
} | ||
return Promise.all(promises); | ||
} | ||
else { | ||
this.logger.warn(this, 'missing handler for action', action); | ||
this.logger.warn(this, 'Missing handler for action', action); | ||
return Promise.reject("Missing handler for action '" + action.kind + "'"); | ||
} | ||
}; | ||
ActionDispatcher.prototype.handleBlocked = function (action, predicate) { | ||
var _this = this; | ||
if (predicate(action)) { | ||
this.blockUntil = undefined; | ||
var result = this.handleAction(action); | ||
var actions = this.postponedActions; | ||
this.postponedActions = []; | ||
for (var _i = 0, actions_1 = actions; _i < actions_1.length; _i++) { | ||
var a = actions_1[_i]; | ||
this.dispatch(a.action).then(a.resolve, a.reject); | ||
} | ||
return result; | ||
} | ||
else { | ||
this.logger.log(this, 'Action is postponed due to block condition', action); | ||
return new Promise(function (resolve, reject) { | ||
_this.postponedActions.push({ action: action, resolve: resolve, reject: reject }); | ||
}); | ||
} | ||
}; | ||
ActionDispatcher = __decorate([ | ||
@@ -101,0 +110,0 @@ inversify_1.injectable(), |
@@ -82,4 +82,3 @@ import { ILogger } from "../../utils/logging"; | ||
protected currentPromise: Promise<CommandStackState>; | ||
constructor(modelFactory: IModelFactory, viewerProvider: IViewerProvider, logger: ILogger, syncer: AnimationFrameSyncer, options: CommandStackOptions); | ||
protected viewer: IViewer; | ||
protected viewer?: IViewer; | ||
protected undoStack: ICommand[]; | ||
@@ -100,2 +99,3 @@ protected redoStack: ICommand[]; | ||
protected offStack: SystemCommand[]; | ||
constructor(modelFactory: IModelFactory, viewerProvider: IViewerProvider, logger: ILogger, syncer: AnimationFrameSyncer, options: CommandStackOptions); | ||
protected readonly currentModel: Promise<SModelRoot>; | ||
@@ -124,11 +124,11 @@ executeAll(commands: ICommand[]): Promise<SModelRoot>; | ||
*/ | ||
updatePopup(model: SModelRoot): void; | ||
update(model: SModelRoot): Promise<void>; | ||
/** | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
*/ | ||
update(model: SModelRoot): void; | ||
updateHidden(model: SModelRoot): Promise<void>; | ||
/** | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
*/ | ||
updateHidden(model: SModelRoot): void; | ||
updatePopup(model: SModelRoot): Promise<void>; | ||
/** | ||
@@ -135,0 +135,0 @@ * Handling of commands after their execution. |
@@ -28,2 +28,37 @@ "use strict"; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [0, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -149,3 +184,3 @@ var inversify_1 = require("inversify"); | ||
this.currentPromise = this.currentPromise.then(function (state) { | ||
var promise = new Promise(function (resolve, reject) { | ||
return new Promise(function (resolve, reject) { | ||
var context = _this.createContext(state.root); | ||
@@ -189,3 +224,2 @@ var newResult; | ||
}); | ||
return promise; | ||
}); | ||
@@ -204,18 +238,34 @@ }; | ||
var _this = this; | ||
this.currentPromise = this.currentPromise.then(function (state) { | ||
if (state.hiddenRootChanged && state.hiddenRoot !== undefined) | ||
_this.updateHidden(state.hiddenRoot); | ||
if (state.rootChanged) | ||
_this.update(state.root); | ||
if (state.popupChanged && state.popupRoot !== undefined) | ||
_this.updatePopup(state.popupRoot); | ||
return { | ||
root: state.root, | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}; | ||
}); | ||
this.currentPromise = this.currentPromise.then(function (state) { return __awaiter(_this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!(state.hiddenRootChanged && state.hiddenRoot !== undefined)) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, this.updateHidden(state.hiddenRoot)]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
if (!state.rootChanged) return [3 /*break*/, 4]; | ||
return [4 /*yield*/, this.update(state.root)]; | ||
case 3: | ||
_a.sent(); | ||
_a.label = 4; | ||
case 4: | ||
if (!(state.popupChanged && state.popupRoot !== undefined)) return [3 /*break*/, 6]; | ||
return [4 /*yield*/, this.updatePopup(state.popupRoot)]; | ||
case 5: | ||
_a.sent(); | ||
_a.label = 6; | ||
case 6: return [2 /*return*/, { | ||
root: state.root, | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}]; | ||
} | ||
}); | ||
}); }); | ||
return this.currentModel; | ||
@@ -226,39 +276,63 @@ }; | ||
*/ | ||
CommandStack.prototype.updatePopup = function (model) { | ||
var _this = this; | ||
if (this.viewer) { | ||
this.viewer.updatePopup(model); | ||
return; | ||
} | ||
this.viewerProvider().then(function (viewer) { | ||
_this.viewer = viewer; | ||
_this.updatePopup(model); | ||
CommandStack.prototype.update = function (model) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
if (!(this.viewer === undefined)) return [3 /*break*/, 2]; | ||
_a = this; | ||
return [4 /*yield*/, this.viewerProvider()]; | ||
case 1: | ||
_a.viewer = _b.sent(); | ||
_b.label = 2; | ||
case 2: | ||
this.viewer.update(model); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
*/ | ||
CommandStack.prototype.update = function (model) { | ||
var _this = this; | ||
if (this.viewer) { | ||
this.viewer.update(model); | ||
return; | ||
} | ||
this.viewerProvider().then(function (viewer) { | ||
_this.viewer = viewer; | ||
_this.update(model); | ||
CommandStack.prototype.updateHidden = function (model) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
if (!(this.viewer === undefined)) return [3 /*break*/, 2]; | ||
_a = this; | ||
return [4 /*yield*/, this.viewerProvider()]; | ||
case 1: | ||
_a.viewer = _b.sent(); | ||
_b.label = 2; | ||
case 2: | ||
this.viewer.updateHidden(model); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
*/ | ||
CommandStack.prototype.updateHidden = function (model) { | ||
var _this = this; | ||
if (this.viewer) { | ||
this.viewer.updateHidden(model); | ||
return; | ||
} | ||
this.viewerProvider().then(function (viewer) { | ||
_this.viewer = viewer; | ||
_this.updateHidden(model); | ||
CommandStack.prototype.updatePopup = function (model) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
if (!(this.viewer === undefined)) return [3 /*break*/, 2]; | ||
_a = this; | ||
return [4 /*yield*/, this.viewerProvider()]; | ||
case 1: | ||
_a.viewer = _b.sent(); | ||
_b.label = 2; | ||
case 2: | ||
this.viewer.updatePopup(model); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
@@ -345,3 +419,3 @@ }; | ||
CommandStack.prototype.createContext = function (currentModel) { | ||
var context = { | ||
return { | ||
root: currentModel, | ||
@@ -354,3 +428,2 @@ modelChanged: this, | ||
}; | ||
return context; | ||
}; | ||
@@ -357,0 +430,0 @@ CommandStack = __decorate([ |
@@ -24,3 +24,7 @@ import { ILogger } from "../../utils/logging"; | ||
export interface ICommand { | ||
readonly blockUntilActionKind?: string; | ||
/** | ||
* If this property is present, all following actions are blocked | ||
* until the function returns `true`. | ||
*/ | ||
readonly blockUntil?: (action: Action) => boolean; | ||
execute(context: CommandExecutionContext): CommandResult; | ||
@@ -27,0 +31,0 @@ undo(context: CommandExecutionContext): CommandResult; |
@@ -36,3 +36,3 @@ import { Action } from "../actions/action"; | ||
redo(context: CommandExecutionContext): SModelRoot; | ||
readonly blockUntilActionKind: string; | ||
readonly blockUntil: (action: Action) => boolean; | ||
} |
@@ -74,5 +74,5 @@ "use strict"; | ||
}; | ||
Object.defineProperty(SetModelCommand.prototype, "blockUntilActionKind", { | ||
Object.defineProperty(SetModelCommand.prototype, "blockUntil", { | ||
get: function () { | ||
return initialize_canvas_1.InitializeCanvasBoundsCommand.KIND; | ||
return function (action) { return action.kind === initialize_canvas_1.InitializeCanvasBoundsCommand.KIND; }; | ||
}, | ||
@@ -79,0 +79,0 @@ enumerable: true, |
@@ -21,2 +21,3 @@ export declare const TYPES: { | ||
IModelFactory: symbol; | ||
IModelLayoutEngine: symbol; | ||
ModelRendererFactory: symbol; | ||
@@ -27,2 +28,3 @@ ModelSource: symbol; | ||
PopupModelFactory: symbol; | ||
IPopupModelProvider: symbol; | ||
PopupMouseListener: symbol; | ||
@@ -29,0 +31,0 @@ PopupVNodeDecorator: symbol; |
@@ -29,2 +29,3 @@ "use strict"; | ||
IModelFactory: Symbol('IModelFactory'), | ||
IModelLayoutEngine: Symbol('IModelLayoutEngine'), | ||
ModelRendererFactory: Symbol('ModelRendererFactory'), | ||
@@ -34,3 +35,7 @@ ModelSource: Symbol('ModelSource'), | ||
MouseListener: Symbol('MouseListener'), | ||
/** | ||
* @deprecated Use IPopupModelProvider instead. | ||
*/ | ||
PopupModelFactory: Symbol('PopupModelFactory'), | ||
IPopupModelProvider: Symbol('IPopupModelProvider'), | ||
PopupMouseListener: Symbol('PopupMouseListener'), | ||
@@ -37,0 +42,0 @@ PopupVNodeDecorator: Symbol('PopupVNodeDecorator'), |
@@ -79,3 +79,3 @@ import { Bounds, Point } from "../../utils/geometry"; | ||
execute(context: CommandExecutionContext): SModelRoot; | ||
readonly blockUntilActionKind: string; | ||
readonly blockUntil: (action: Action) => boolean; | ||
} |
@@ -109,5 +109,5 @@ "use strict"; | ||
}; | ||
Object.defineProperty(RequestBoundsCommand.prototype, "blockUntilActionKind", { | ||
Object.defineProperty(RequestBoundsCommand.prototype, "blockUntil", { | ||
get: function () { | ||
return ComputedBoundsAction.KIND; | ||
return function (action) { return action.kind === ComputedBoundsAction.KIND; }; | ||
}, | ||
@@ -114,0 +114,0 @@ enumerable: true, |
@@ -44,4 +44,4 @@ import { Point } from '../../utils/geometry'; | ||
protected doMove(context: CommandExecutionContext, reverse?: boolean): SModelRoot; | ||
undo(context: CommandExecutionContext): SModelRoot | Promise<SModelRoot>; | ||
redo(context: CommandExecutionContext): SModelRoot | Promise<SModelRoot>; | ||
undo(context: CommandExecutionContext): Promise<SModelRoot>; | ||
redo(context: CommandExecutionContext): Promise<SModelRoot>; | ||
merge(command: ICommand, context: CommandExecutionContext): boolean; | ||
@@ -48,0 +48,0 @@ } |
@@ -128,16 +128,6 @@ "use strict"; | ||
MoveCommand.prototype.undo = function (context) { | ||
if (this.action.animate) { | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start(); | ||
} | ||
else { | ||
return this.doMove(context, true); | ||
} | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start(); | ||
}; | ||
MoveCommand.prototype.redo = function (context) { | ||
if (this.action.animate) { | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start(); | ||
} | ||
else { | ||
return this.doMove(context, false); | ||
} | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start(); | ||
}; | ||
@@ -144,0 +134,0 @@ MoveCommand.prototype.merge = function (command, context) { |
@@ -7,3 +7,10 @@ import { Point } from "../utils/geometry"; | ||
} | ||
export declare type EdgeRouter = (edge: SEdge) => RoutedPoint[]; | ||
export declare function linearRoute(edge: SEdge): RoutedPoint[]; | ||
export interface IEdgeRouter { | ||
route(edge: SEdge): RoutedPoint[]; | ||
} | ||
export interface LinearRouteOptions { | ||
minimalPointDistance: number; | ||
} | ||
export declare class LinearEdgeRouter implements IEdgeRouter { | ||
route(edge: SEdge, options?: LinearRouteOptions): RoutedPoint[]; | ||
} |
@@ -8,44 +8,59 @@ "use strict"; | ||
*/ | ||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | ||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | ||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | ||
return c > 3 && r && Object.defineProperty(target, key, r), r; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var inversify_1 = require("inversify"); | ||
var geometry_1 = require("../utils/geometry"); | ||
var minimalPointDistance = 2; | ||
function linearRoute(edge) { | ||
var source = edge.source; | ||
var target = edge.target; | ||
if (source === undefined || target === undefined) { | ||
return []; | ||
var LinearEdgeRouter = /** @class */ (function () { | ||
function LinearEdgeRouter() { | ||
} | ||
var sourceAnchor; | ||
var targetAnchor; | ||
var rpCount = edge.routingPoints !== undefined ? edge.routingPoints.length : 0; | ||
if (rpCount >= 1) { | ||
// Use the first routing point as start anchor reference | ||
var p0 = edge.routingPoints[0]; | ||
sourceAnchor = source.getTranslatedAnchor(p0, edge.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the last routing point as end anchor reference | ||
var pn = edge.routingPoints[rpCount - 1]; | ||
targetAnchor = target.getTranslatedAnchor(pn, edge.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
else { | ||
// Use the target center as start anchor reference | ||
var startRef = geometry_1.center(target.bounds); | ||
sourceAnchor = source.getTranslatedAnchor(startRef, target.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the source center as end anchor reference | ||
var endRef = geometry_1.center(source.bounds); | ||
targetAnchor = target.getTranslatedAnchor(endRef, source.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
var result = []; | ||
result.push({ kind: 'source', x: sourceAnchor.x, y: sourceAnchor.y }); | ||
for (var i = 0; i < rpCount; i++) { | ||
var p = edge.routingPoints[i]; | ||
if (i > 0 && i < rpCount - 1 | ||
|| i === 0 && geometry_1.maxDistance(sourceAnchor, p) >= minimalPointDistance + (edge.sourceAnchorCorrection || 0) | ||
|| i === rpCount - 1 && geometry_1.maxDistance(p, targetAnchor) >= minimalPointDistance + (edge.targetAnchorCorrection || 0)) { | ||
result.push({ kind: 'linear', x: p.x, y: p.y, pointIndex: i }); | ||
LinearEdgeRouter.prototype.route = function (edge, options) { | ||
if (options === void 0) { options = { minimalPointDistance: 2 }; } | ||
var source = edge.source; | ||
var target = edge.target; | ||
if (source === undefined || target === undefined) { | ||
return []; | ||
} | ||
} | ||
result.push({ kind: 'target', x: targetAnchor.x, y: targetAnchor.y }); | ||
return result; | ||
} | ||
exports.linearRoute = linearRoute; | ||
var sourceAnchor; | ||
var targetAnchor; | ||
var rpCount = edge.routingPoints !== undefined ? edge.routingPoints.length : 0; | ||
if (rpCount >= 1) { | ||
// Use the first routing point as start anchor reference | ||
var p0 = edge.routingPoints[0]; | ||
sourceAnchor = source.getTranslatedAnchor(p0, edge.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the last routing point as end anchor reference | ||
var pn = edge.routingPoints[rpCount - 1]; | ||
targetAnchor = target.getTranslatedAnchor(pn, edge.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
else { | ||
// Use the target center as start anchor reference | ||
var startRef = geometry_1.center(target.bounds); | ||
sourceAnchor = source.getTranslatedAnchor(startRef, target.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the source center as end anchor reference | ||
var endRef = geometry_1.center(source.bounds); | ||
targetAnchor = target.getTranslatedAnchor(endRef, source.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
var result = []; | ||
result.push({ kind: 'source', x: sourceAnchor.x, y: sourceAnchor.y }); | ||
for (var i = 0; i < rpCount; i++) { | ||
var p = edge.routingPoints[i]; | ||
if (i > 0 && i < rpCount - 1 | ||
|| i === 0 && geometry_1.maxDistance(sourceAnchor, p) >= options.minimalPointDistance + (edge.sourceAnchorCorrection || 0) | ||
|| i === rpCount - 1 && geometry_1.maxDistance(p, targetAnchor) >= options.minimalPointDistance + (edge.targetAnchorCorrection || 0)) { | ||
result.push({ kind: 'linear', x: p.x, y: p.y, pointIndex: i }); | ||
} | ||
} | ||
result.push({ kind: 'target', x: targetAnchor.x, y: targetAnchor.y }); | ||
return result; | ||
}; | ||
LinearEdgeRouter = __decorate([ | ||
inversify_1.injectable() | ||
], LinearEdgeRouter); | ||
return LinearEdgeRouter; | ||
}()); | ||
exports.LinearEdgeRouter = LinearEdgeRouter; | ||
//# sourceMappingURL=routing.js.map |
@@ -11,3 +11,3 @@ import { FluentIterable } from '../utils/iterable'; | ||
import { Routable } from '../features/edit/model'; | ||
import { RoutedPoint } from './routing'; | ||
import { RoutedPoint, IEdgeRouter } from './routing'; | ||
/** | ||
@@ -134,2 +134,3 @@ * Serializable schema for graph-like models. | ||
targetAnchorCorrection?: number; | ||
router?: IEdgeRouter; | ||
readonly source: SConnectableElement | undefined; | ||
@@ -136,0 +137,0 @@ readonly target: SConnectableElement | undefined; |
@@ -179,3 +179,5 @@ "use strict"; | ||
SEdge.prototype.route = function () { | ||
var route = routing_1.linearRoute(this); | ||
if (this.router === undefined) | ||
this.router = new routing_1.LinearEdgeRouter(); | ||
var route = this.router.route(this); | ||
return model_7.filterEditModeHandles(route, this); | ||
@@ -182,0 +184,0 @@ }; |
@@ -44,3 +44,3 @@ "use strict"; | ||
return JSX.createElement("g", null, | ||
JSX.createElement("rect", { "class-sprotty-node": node instanceof sgraph_1.SNode, "class-sprotty-port": node instanceof sgraph_1.SPort, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, x: "0", y: "0", width: node.size.width, height: node.size.height }), | ||
JSX.createElement("rect", { "class-sprotty-node": node instanceof sgraph_1.SNode, "class-sprotty-port": node instanceof sgraph_1.SPort, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, x: "0", y: "0", width: Math.max(node.size.width, 0), height: Math.max(node.size.height, 0) }), | ||
context.renderChildren(node)); | ||
@@ -47,0 +47,0 @@ }; |
@@ -106,2 +106,3 @@ "use strict"; | ||
DiagramServer.prototype.messageReceived = function (data) { | ||
var _this = this; | ||
var object = typeof (data) === 'string' ? JSON.parse(data) : data; | ||
@@ -112,3 +113,5 @@ if (isActionMessage(object) && object.action) { | ||
this.logger.log(this, 'receiving', object); | ||
this.actionDispatcher.dispatch(object.action, this.storeNewModel.bind(this)); | ||
this.actionDispatcher.dispatch(object.action).then(function () { | ||
_this.storeNewModel(object.action); | ||
}); | ||
} | ||
@@ -115,0 +118,0 @@ } |
import { Bounds, Point } from "../utils/geometry"; | ||
import { Deferred } from "../utils/async"; | ||
import { ILogger } from "../utils/logging"; | ||
import { Action } from "../base/actions/action"; | ||
@@ -7,3 +9,3 @@ import { ActionHandlerRegistry } from "../base/actions/action-handler"; | ||
import { RequestModelAction } from "../base/features/set-model"; | ||
import { SModelElementSchema, SModelRootSchema } from "../base/model/smodel"; | ||
import { SModelElementSchema, SModelIndex, SModelRootSchema } from "../base/model/smodel"; | ||
import { ComputedBoundsAction } from '../features/bounds/bounds-manipulation'; | ||
@@ -16,33 +18,67 @@ import { Match } from "../features/update/model-matching"; | ||
import { DiagramState } from './diagram-state'; | ||
export declare type PopupModelFactory = (request: RequestPopupModelAction, element?: SModelElementSchema) => SModelRootSchema | undefined; | ||
export interface IStateAwareModelProvider { | ||
getModel(diagramState: DiagramState, currentRoot?: SModelRootSchema): SModelRootSchema; | ||
} | ||
/** | ||
* A model source that handles actions for bounds calculation and model | ||
* updates. | ||
* A model source that allows to set and modify the model through function calls. | ||
* This class can be used as a facade over the action-based API of sprotty. It handles | ||
* actions for bounds calculation and model updates. | ||
*/ | ||
export declare class LocalModelSource extends ModelSource { | ||
protected popupModelFactory: PopupModelFactory | undefined; | ||
protected readonly logger: ILogger; | ||
protected modelProvider: IStateAwareModelProvider | undefined; | ||
protected popupModelProvider: IPopupModelProvider | undefined; | ||
protected layoutEngine: IModelLayoutEngine | undefined; | ||
protected currentRoot: SModelRootSchema; | ||
protected diagramState: DiagramState; | ||
/** | ||
* The `type` property of the model root is used to determine whether a model update | ||
* is a change of the previous model or a totally new one. | ||
*/ | ||
protected lastSubmittedModelType: string; | ||
/** | ||
* When client layout is active, model updates are not applied immediately. Instead the | ||
* model is rendered on a hidden canvas first to derive actual bounds. The promises listed | ||
* here are resolved after the new bounds have been applied and the new model state has | ||
* been actually applied to the visible canvas. | ||
*/ | ||
protected pendingUpdates: Deferred<void>[]; | ||
model: SModelRootSchema; | ||
protected onModelSubmitted: (newRoot: SModelRootSchema) => void; | ||
protected diagramState: DiagramState; | ||
constructor(actionDispatcher: IActionDispatcher, actionHandlerRegistry: ActionHandlerRegistry, viewerOptions: ViewerOptions, popupModelFactory?: PopupModelFactory | undefined, modelProvider?: IStateAwareModelProvider | undefined); | ||
constructor(actionDispatcher: IActionDispatcher, actionHandlerRegistry: ActionHandlerRegistry, viewerOptions: ViewerOptions, logger: ILogger, modelProvider?: IStateAwareModelProvider | undefined, popupModelProvider?: IPopupModelProvider | undefined, layoutEngine?: IModelLayoutEngine | undefined); | ||
protected initialize(registry: ActionHandlerRegistry): void; | ||
setModel(newRoot: SModelRootSchema): void; | ||
updateModel(newRoot?: SModelRootSchema): void; | ||
protected submitModel(newRoot: SModelRootSchema, update: boolean): void; | ||
protected doSubmitModel(newRoot: SModelRootSchema, update: boolean): void; | ||
applyMatches(matches: Match[]): void; | ||
/** | ||
* Set the model without incremental update. | ||
*/ | ||
setModel(newRoot: SModelRootSchema): Promise<void>; | ||
/** | ||
* Apply an incremental update to the model with an animation showing the transition to | ||
* the new state. If `newRoot` is undefined, the current root is submitted; in that case | ||
* it is assumed that it has been modified before. | ||
*/ | ||
updateModel(newRoot?: SModelRootSchema): Promise<void>; | ||
/** | ||
* If client layout is active, run a `RequestBoundsAction` and wait for the resulting | ||
* `ComputedBoundsAction`, otherwise call `doSubmitModel(…)` directly. | ||
*/ | ||
protected submitModel(newRoot: SModelRootSchema, update: boolean | Match[]): Promise<void>; | ||
/** | ||
* Submit the given model with an `UpdateModelAction` or a `SetModelAction` depending on the | ||
* `update` argument. If available, the model layout engine is invoked first. | ||
*/ | ||
protected doSubmitModel(newRoot: SModelRootSchema, update: boolean | Match[], index?: SModelIndex<SModelElementSchema>): Promise<void>; | ||
/** | ||
* Modify the current model with an array of matches. | ||
*/ | ||
applyMatches(matches: Match[]): Promise<void>; | ||
/** | ||
* Modify the current model by adding new elements. | ||
*/ | ||
addElements(elements: (SModelElementSchema | { | ||
element: SModelElementSchema; | ||
parentId: string; | ||
})[]): void; | ||
})[]): Promise<void>; | ||
/** | ||
* Modify the current model by removing elements. | ||
*/ | ||
removeElements(elements: (string | { | ||
elementId: string; | ||
parentId: string; | ||
})[]): void; | ||
})[]): Promise<void>; | ||
handle(action: Action): void; | ||
@@ -58,1 +94,14 @@ protected handleRequestModel(action: RequestModelAction): void; | ||
} | ||
/** | ||
* @deprecated Use IPopupModelProvider instead. | ||
*/ | ||
export declare type PopupModelFactory = (request: RequestPopupModelAction, element?: SModelElementSchema) => SModelRootSchema | undefined; | ||
export interface IPopupModelProvider { | ||
getPopupModel(request: RequestPopupModelAction, element?: SModelElementSchema): SModelRootSchema | undefined; | ||
} | ||
export interface IStateAwareModelProvider { | ||
getModel(diagramState: DiagramState, currentRoot?: SModelRootSchema): SModelRootSchema; | ||
} | ||
export interface IModelLayoutEngine { | ||
layout(model: SModelRootSchema, index?: SModelIndex<SModelElementSchema>): SModelRootSchema | Promise<SModelRootSchema>; | ||
} |
@@ -30,4 +30,40 @@ "use strict"; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [0, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var inversify_1 = require("inversify"); | ||
var async_1 = require("../utils/async"); | ||
var types_1 = require("../base/types"); | ||
@@ -48,11 +84,14 @@ var action_handler_1 = require("../base/actions/action-handler"); | ||
/** | ||
* A model source that handles actions for bounds calculation and model | ||
* updates. | ||
* A model source that allows to set and modify the model through function calls. | ||
* This class can be used as a facade over the action-based API of sprotty. It handles | ||
* actions for bounds calculation and model updates. | ||
*/ | ||
var LocalModelSource = /** @class */ (function (_super) { | ||
__extends(LocalModelSource, _super); | ||
function LocalModelSource(actionDispatcher, actionHandlerRegistry, viewerOptions, popupModelFactory, modelProvider) { | ||
function LocalModelSource(actionDispatcher, actionHandlerRegistry, viewerOptions, logger, modelProvider, popupModelProvider, layoutEngine) { | ||
var _this = _super.call(this, actionDispatcher, actionHandlerRegistry, viewerOptions) || this; | ||
_this.popupModelFactory = popupModelFactory; | ||
_this.logger = logger; | ||
_this.modelProvider = modelProvider; | ||
_this.popupModelProvider = popupModelProvider; | ||
_this.layoutEngine = layoutEngine; | ||
_this.currentRoot = { | ||
@@ -65,2 +104,9 @@ type: 'NONE', | ||
}; | ||
/** | ||
* When client layout is active, model updates are not applied immediately. Instead the | ||
* model is rendered on a hidden canvas first to derive actual bounds. The promises listed | ||
* here are resolved after the new bounds have been applied and the new model state has | ||
* been actually applied to the visible canvas. | ||
*/ | ||
_this.pendingUpdates = []; | ||
return _this; | ||
@@ -88,2 +134,5 @@ } | ||
}; | ||
/** | ||
* Set the model without incremental update. | ||
*/ | ||
LocalModelSource.prototype.setModel = function (newRoot) { | ||
@@ -94,48 +143,95 @@ this.currentRoot = newRoot; | ||
}; | ||
this.submitModel(newRoot, false); | ||
return this.submitModel(newRoot, false); | ||
}; | ||
/** | ||
* Apply an incremental update to the model with an animation showing the transition to | ||
* the new state. If `newRoot` is undefined, the current root is submitted; in that case | ||
* it is assumed that it has been modified before. | ||
*/ | ||
LocalModelSource.prototype.updateModel = function (newRoot) { | ||
if (newRoot === undefined) { | ||
this.submitModel(this.currentRoot, true); | ||
return this.submitModel(this.currentRoot, true); | ||
} | ||
else { | ||
this.currentRoot = newRoot; | ||
this.submitModel(newRoot, true); | ||
return this.submitModel(newRoot, true); | ||
} | ||
}; | ||
/** | ||
* If client layout is active, run a `RequestBoundsAction` and wait for the resulting | ||
* `ComputedBoundsAction`, otherwise call `doSubmitModel(…)` directly. | ||
*/ | ||
LocalModelSource.prototype.submitModel = function (newRoot, update) { | ||
if (this.viewerOptions.needsClientLayout) { | ||
var deferred = new async_1.Deferred(); | ||
this.pendingUpdates.push(deferred); | ||
this.actionDispatcher.dispatch(new bounds_manipulation_1.RequestBoundsAction(newRoot)); | ||
return deferred.promise; | ||
} | ||
else { | ||
this.doSubmitModel(newRoot, update); | ||
return this.doSubmitModel(newRoot, update); | ||
} | ||
}; | ||
LocalModelSource.prototype.doSubmitModel = function (newRoot, update) { | ||
if (update && newRoot.type === this.lastSubmittedModelType) { | ||
this.actionDispatcher.dispatch(new update_model_1.UpdateModelAction(newRoot)); | ||
} | ||
else { | ||
this.actionDispatcher.dispatch(new set_model_1.SetModelAction(newRoot)); | ||
} | ||
this.lastSubmittedModelType = newRoot.type; | ||
if (this.onModelSubmitted !== undefined) { | ||
this.onModelSubmitted(newRoot); | ||
} | ||
/** | ||
* Submit the given model with an `UpdateModelAction` or a `SetModelAction` depending on the | ||
* `update` argument. If available, the model layout engine is invoked first. | ||
*/ | ||
LocalModelSource.prototype.doSubmitModel = function (newRoot, update, index) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var layoutResult, error_1, lastSubmittedModelType, updates, input; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!(this.layoutEngine !== undefined)) return [3 /*break*/, 6]; | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 5, , 6]); | ||
layoutResult = this.layoutEngine.layout(newRoot, index); | ||
if (!(layoutResult instanceof Promise)) return [3 /*break*/, 3]; | ||
return [4 /*yield*/, layoutResult]; | ||
case 2: | ||
newRoot = _a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
if (layoutResult !== undefined) | ||
newRoot = layoutResult; | ||
_a.label = 4; | ||
case 4: return [3 /*break*/, 6]; | ||
case 5: | ||
error_1 = _a.sent(); | ||
this.logger.error(this, error_1.toString(), error_1.stack); | ||
return [3 /*break*/, 6]; | ||
case 6: | ||
lastSubmittedModelType = this.lastSubmittedModelType; | ||
this.lastSubmittedModelType = newRoot.type; | ||
updates = this.pendingUpdates; | ||
this.pendingUpdates = []; | ||
if (!(update && newRoot.type === lastSubmittedModelType)) return [3 /*break*/, 8]; | ||
input = Array.isArray(update) ? update : newRoot; | ||
return [4 /*yield*/, this.actionDispatcher.dispatch(new update_model_1.UpdateModelAction(input))]; | ||
case 7: | ||
_a.sent(); | ||
return [3 /*break*/, 10]; | ||
case 8: return [4 /*yield*/, this.actionDispatcher.dispatch(new set_model_1.SetModelAction(newRoot))]; | ||
case 9: | ||
_a.sent(); | ||
_a.label = 10; | ||
case 10: | ||
updates.forEach(function (d) { return d.resolve(); }); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Modify the current model with an array of matches. | ||
*/ | ||
LocalModelSource.prototype.applyMatches = function (matches) { | ||
var root = this.currentRoot; | ||
model_matching_1.applyMatches(root, matches); | ||
if (this.viewerOptions.needsClientLayout) { | ||
this.actionDispatcher.dispatch(new bounds_manipulation_1.RequestBoundsAction(root)); | ||
} | ||
else { | ||
var update = new update_model_1.UpdateModelAction(matches); | ||
this.actionDispatcher.dispatch(update); | ||
this.lastSubmittedModelType = root.type; | ||
if (this.onModelSubmitted !== undefined) { | ||
this.onModelSubmitted(root); | ||
} | ||
} | ||
return this.submitModel(root, matches); | ||
}; | ||
/** | ||
* Modify the current model by adding new elements. | ||
*/ | ||
LocalModelSource.prototype.addElements = function (elements) { | ||
@@ -159,4 +255,7 @@ var matches = []; | ||
} | ||
this.applyMatches(matches); | ||
return this.applyMatches(matches); | ||
}; | ||
/** | ||
* Modify the current model by removing elements. | ||
*/ | ||
LocalModelSource.prototype.removeElements = function (elements) { | ||
@@ -188,4 +287,5 @@ var matches = []; | ||
} | ||
this.applyMatches(matches); | ||
return this.applyMatches(matches); | ||
}; | ||
// ----- Methods for handling incoming actions ---------------------------- | ||
LocalModelSource.prototype.handle = function (action) { | ||
@@ -236,3 +336,3 @@ switch (action.kind) { | ||
} | ||
this.doSubmitModel(root, true); | ||
this.doSubmitModel(root, true, index); | ||
}; | ||
@@ -249,5 +349,5 @@ LocalModelSource.prototype.applyBounds = function (element, newBounds) { | ||
LocalModelSource.prototype.handleRequestPopupModel = function (action) { | ||
if (this.popupModelFactory !== undefined) { | ||
if (this.popupModelProvider !== undefined) { | ||
var element = smodel_utils_1.findElement(this.currentRoot, action.elementId); | ||
var popupRoot = this.popupModelFactory(action, element); | ||
var popupRoot = this.popupModelProvider.getPopupModel(action, element); | ||
if (popupRoot !== undefined) { | ||
@@ -287,5 +387,7 @@ popupRoot.canvasBounds = action.bounds; | ||
__param(2, inversify_1.inject(types_1.TYPES.ViewerOptions)), | ||
__param(3, inversify_1.inject(types_1.TYPES.PopupModelFactory)), __param(3, inversify_1.optional()), | ||
__param(3, inversify_1.inject(types_1.TYPES.ILogger)), | ||
__param(4, inversify_1.inject(types_1.TYPES.StateAwareModelProvider)), __param(4, inversify_1.optional()), | ||
__metadata("design:paramtypes", [Object, action_handler_1.ActionHandlerRegistry, Object, Function, Object]) | ||
__param(5, inversify_1.inject(types_1.TYPES.IPopupModelProvider)), __param(5, inversify_1.optional()), | ||
__param(6, inversify_1.inject(types_1.TYPES.IModelLayoutEngine)), __param(6, inversify_1.optional()), | ||
__metadata("design:paramtypes", [Object, action_handler_1.ActionHandlerRegistry, Object, Object, Object, Object, Object]) | ||
], LocalModelSource); | ||
@@ -292,0 +394,0 @@ return LocalModelSource; |
{ | ||
"name": "sprotty", | ||
"version": "0.4.0-next.f74c0b42", | ||
"version": "0.4.0", | ||
"description": "A next-gen framework for graphical views", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
@@ -25,31 +25,2 @@ /* | ||
let execCount = 0; | ||
let undoCount = 0; | ||
let redoCount = 0; | ||
const mockCommandStack: ICommandStack = { | ||
execute(command: ICommand) { | ||
++execCount; | ||
return null!; | ||
}, | ||
executeAll(commands: ICommand[]) { | ||
++execCount; | ||
return null!; | ||
}, | ||
undo() { | ||
++undoCount; | ||
return null!; | ||
}, | ||
redo() { | ||
++redoCount; | ||
return null!; | ||
} | ||
}; | ||
const container = new Container(); | ||
container.load(defaultModule); | ||
container.rebind(TYPES.ICommandStack).toConstantValue(mockCommandStack); | ||
const actionDispatcher = container.get<IActionDispatcher>(TYPES.IActionDispatcher); | ||
class MockCommand extends Command { | ||
@@ -75,44 +46,95 @@ static KIND = 'mock'; | ||
it('undo/redo/execute', () => { | ||
function setup() { | ||
const state = { | ||
execCount: 0, | ||
undoCount: 0, | ||
redoCount: 0 | ||
} | ||
const mockCommandStack: ICommandStack = { | ||
execute(command: ICommand): Promise<any> { | ||
++state.execCount; | ||
return Promise.resolve(); | ||
}, | ||
executeAll(commands: ICommand[]): Promise<any> { | ||
++state.execCount; | ||
return Promise.resolve(); | ||
}, | ||
undo(): Promise<any> { | ||
++state.undoCount; | ||
return Promise.resolve(); | ||
}, | ||
redo(): Promise<any> { | ||
++state.redoCount; | ||
return Promise.resolve(); | ||
} | ||
}; | ||
const container = new Container(); | ||
container.load(defaultModule); | ||
container.rebind(TYPES.ICommandStack).toConstantValue(mockCommandStack); | ||
const actionDispatcher = container.get<IActionDispatcher>(TYPES.IActionDispatcher); | ||
const registry = container.get<ActionHandlerRegistry>(TYPES.ActionHandlerRegistry); | ||
return { actionDispatcher, registry, state }; | ||
} | ||
it('should execute/undo/redo', () => { | ||
const { actionDispatcher, registry, state } = setup(); | ||
// an initial SetModelAction is fired automatically | ||
expect(execCount).to.be.equal(1); | ||
expect(undoCount).to.be.equal(0); | ||
expect(redoCount).to.be.equal(0); | ||
expect(state.execCount).to.be.equal(1); | ||
expect(state.undoCount).to.be.equal(0); | ||
expect(state.redoCount).to.be.equal(0); | ||
// actions are postponed until InitializeCanvasBoundsAction comes in | ||
actionDispatcher.dispatch(new UndoAction); | ||
expect(execCount).to.be.equal(1); | ||
expect(undoCount).to.be.equal(0); | ||
expect(redoCount).to.be.equal(0); | ||
expect(state.execCount).to.be.equal(1); | ||
expect(state.undoCount).to.be.equal(0); | ||
expect(state.redoCount).to.be.equal(0); | ||
actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS)); | ||
// postponed actions are fired as well | ||
expect(execCount).to.be.equal(2); | ||
expect(undoCount).to.be.equal(1); | ||
expect(redoCount).to.be.equal(0); | ||
expect(state.execCount).to.be.equal(2); | ||
expect(state.undoCount).to.be.equal(1); | ||
expect(state.redoCount).to.be.equal(0); | ||
actionDispatcher.dispatch(new RedoAction); | ||
expect(execCount).to.be.equal(2); | ||
expect(undoCount).to.be.equal(1); | ||
expect(redoCount).to.be.equal(1); | ||
expect(state.execCount).to.be.equal(2); | ||
expect(state.undoCount).to.be.equal(1); | ||
expect(state.redoCount).to.be.equal(1); | ||
actionDispatcher.dispatch({kind: 'unknown'}); | ||
expect(execCount).to.be.equal(2); | ||
expect(undoCount).to.be.equal(1); | ||
expect(redoCount).to.be.equal(1); | ||
actionDispatcher.dispatch({ kind: 'unknown' }).catch(() => {}); | ||
expect(state.execCount).to.be.equal(2); | ||
expect(state.undoCount).to.be.equal(1); | ||
expect(state.redoCount).to.be.equal(1); | ||
// MoveAction is not registered by default | ||
// MockAction is not registered by default | ||
actionDispatcher.dispatch(new MockAction()).catch(() => {}); | ||
expect(state.execCount).to.be.equal(2); | ||
expect(state.undoCount).to.be.equal(1); | ||
expect(state.redoCount).to.be.equal(1); | ||
registry.registerCommand(MockCommand); | ||
actionDispatcher.dispatch(new MockAction()); | ||
expect(execCount).to.be.equal(2); | ||
expect(undoCount).to.be.equal(1); | ||
expect(redoCount).to.be.equal(1); | ||
expect(state.execCount).to.be.equal(3); | ||
expect(state.undoCount).to.be.equal(1); | ||
expect(state.redoCount).to.be.equal(1); | ||
}); | ||
const registry = container.get<ActionHandlerRegistry>(TYPES.ActionHandlerRegistry); | ||
it('should resolve/reject promises', async () => { | ||
const { actionDispatcher, registry } = setup(); | ||
actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS)); | ||
registry.registerCommand(MockCommand); | ||
await actionDispatcher.dispatch(new MockAction()); | ||
// We expect this promis to be resolved | ||
actionDispatcher.dispatch(new MockAction()); | ||
expect(execCount).to.be.equal(3); | ||
expect(undoCount).to.be.equal(1); | ||
expect(redoCount).to.be.equal(1); | ||
try { | ||
await actionDispatcher.dispatch({ kind: 'unknown' }); | ||
expect.fail(); | ||
} catch { | ||
// We expect this promise to be rejected | ||
} | ||
}); | ||
}); |
@@ -20,4 +20,4 @@ /* | ||
export interface IActionDispatcher { | ||
dispatch(action: Action, onExecute?: (action: Action) => void): void | ||
dispatchAll(actions: Action[]): void | ||
dispatch(action: Action): Promise<void> | ||
dispatchAll(actions: Action[]): Promise<void> | ||
} | ||
@@ -32,4 +32,4 @@ | ||
protected blockUntilActionKind: string | undefined; | ||
protected postponedActions: ActionAndHook[]; | ||
protected blockUntil?: (action: Action) => boolean; | ||
protected postponedActions: PostponedAction[] = []; | ||
@@ -42,62 +42,66 @@ constructor(@inject(TYPES.ActionHandlerRegistry) protected actionHandlerRegistry: ActionHandlerRegistry, | ||
const initialCommand = new SetModelCommand(new SetModelAction(EMPTY_ROOT)); | ||
this.blockUntilActionKind = initialCommand.blockUntilActionKind; | ||
this.blockUntil = initialCommand.blockUntil; | ||
this.commandStack.execute(initialCommand); | ||
} | ||
dispatchAll(actions: Action[]): void { | ||
actions.forEach(action => this.dispatch(action)); | ||
dispatchAll(actions: Action[]): Promise<void> { | ||
return Promise.all(actions.map(action => this.dispatch(action))) as Promise<any>; | ||
} | ||
dispatch(action: Action, onExecute?: (action: Action) => void): void { | ||
if (action.kind === this.blockUntilActionKind) { | ||
this.blockUntilActionKind = undefined; | ||
this.handleAction(action); | ||
const actions = this.postponedActions; | ||
this.postponedActions = []; | ||
actions.forEach( | ||
a => this.dispatch(a.action, a.onExecute) | ||
); | ||
return; | ||
} | ||
if (this.blockUntilActionKind !== undefined) { | ||
this.logger.log(this, 'waiting for ' + this.blockUntilActionKind + '. postponing', action); | ||
this.postponedActions.push({ | ||
action: action, | ||
onExecute: onExecute | ||
}); | ||
return; | ||
} | ||
if (onExecute !== undefined) | ||
onExecute.call(null, action); | ||
if (action.kind === UndoAction.KIND) { | ||
this.commandStack.undo(); | ||
dispatch(action: Action): Promise<void> { | ||
if (this.blockUntil !== undefined) { | ||
return this.handleBlocked(action, this.blockUntil); | ||
} else if (action.kind === UndoAction.KIND) { | ||
return this.commandStack.undo().then(() => {}); | ||
} else if (action.kind === RedoAction.KIND) { | ||
this.commandStack.redo(); | ||
return this.commandStack.redo().then(() => {}); | ||
} else { | ||
this.handleAction(action); | ||
return this.handleAction(action); | ||
} | ||
} | ||
protected handleAction(action: Action): void { | ||
protected handleAction(action: Action): Promise<void> { | ||
this.logger.log(this, 'handle', action); | ||
const handlers = this.actionHandlerRegistry.get(action.kind); | ||
if (handlers.length > 0) { | ||
const promises: Promise<any>[] = []; | ||
for (const handler of handlers) { | ||
const result = handler.handle(action); | ||
if (isAction(result)) | ||
this.dispatch(result); | ||
else if (result !== undefined) { | ||
this.commandStack.execute(result); | ||
this.blockUntilActionKind = result.blockUntilActionKind; | ||
if (isAction(result)) { | ||
promises.push(this.dispatch(result)); | ||
} else if (result !== undefined) { | ||
promises.push(this.commandStack.execute(result)); | ||
this.blockUntil = result.blockUntil; | ||
} | ||
} | ||
return Promise.all(promises) as Promise<any>; | ||
} else { | ||
this.logger.warn(this, 'missing handler for action', action); | ||
this.logger.warn(this, 'Missing handler for action', action); | ||
return Promise.reject(`Missing handler for action '${action.kind}'`); | ||
} | ||
} | ||
protected handleBlocked(action: Action, predicate: (action: Action) => boolean): Promise<void> { | ||
if (predicate(action)) { | ||
this.blockUntil = undefined; | ||
const result = this.handleAction(action); | ||
const actions = this.postponedActions; | ||
this.postponedActions = []; | ||
for (const a of actions) { | ||
this.dispatch(a.action).then(a.resolve, a.reject); | ||
} | ||
return result; | ||
} else { | ||
this.logger.log(this, 'Action is postponed due to block condition', action); | ||
return new Promise((resolve, reject) => { | ||
this.postponedActions.push({ action, resolve, reject }); | ||
}); | ||
} | ||
} | ||
} | ||
export interface ActionAndHook { | ||
export interface PostponedAction { | ||
action: Action | ||
onExecute?: (action: Action) => void | ||
resolve: () => void | ||
reject: (reason: any) => void | ||
} |
@@ -97,19 +97,4 @@ /* | ||
constructor(@inject(TYPES.IModelFactory) protected modelFactory: IModelFactory, | ||
@inject(TYPES.IViewerProvider) protected viewerProvider: IViewerProvider, | ||
@inject(TYPES.ILogger) protected logger: ILogger, | ||
@inject(TYPES.AnimationFrameSyncer) protected syncer: AnimationFrameSyncer, | ||
@inject(TYPES.CommandStackOptions) protected options: CommandStackOptions) { | ||
this.currentPromise = Promise.resolve({ | ||
root: modelFactory.createRoot(EMPTY_ROOT), | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}); | ||
} | ||
protected viewer?: IViewer; | ||
protected viewer: IViewer; | ||
protected undoStack: ICommand[] = []; | ||
@@ -132,2 +117,17 @@ protected redoStack: ICommand[] = []; | ||
constructor(@inject(TYPES.IModelFactory) protected modelFactory: IModelFactory, | ||
@inject(TYPES.IViewerProvider) protected viewerProvider: IViewerProvider, | ||
@inject(TYPES.ILogger) protected logger: ILogger, | ||
@inject(TYPES.AnimationFrameSyncer) protected syncer: AnimationFrameSyncer, | ||
@inject(TYPES.CommandStackOptions) protected options: CommandStackOptions) { | ||
this.currentPromise = Promise.resolve({ | ||
root: modelFactory.createRoot(EMPTY_ROOT), | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}); | ||
} | ||
protected get currentModel(): Promise<SModelRoot> { | ||
@@ -192,45 +192,35 @@ return this.currentPromise.then( | ||
beforeResolve: (command: ICommand, context: CommandExecutionContext) => void) { | ||
this.currentPromise = this.currentPromise.then( | ||
state => { | ||
const promise = new Promise( | ||
(resolve: (result: CommandStackState) => void, reject: (reason?: any) => void) => { | ||
const context = this.createContext(state.root); | ||
let newResult: CommandResult; | ||
try { | ||
newResult = operation.call(command, context); | ||
} catch (error) { | ||
this.logger.error(this, "Failed to execute command:", error); | ||
newResult = state.root; | ||
this.currentPromise = this.currentPromise.then(state => | ||
new Promise((resolve: (result: CommandStackState) => void, reject: (reason?: any) => void) => { | ||
const context = this.createContext(state.root); | ||
let newResult: CommandResult; | ||
try { | ||
newResult = operation.call(command, context); | ||
} catch (error) { | ||
this.logger.error(this, "Failed to execute command:", error); | ||
newResult = state.root; | ||
} | ||
if (command instanceof HiddenCommand) { | ||
resolve({ | ||
...state, ...{ | ||
hiddenRoot: newResult as SModelRoot, | ||
hiddenRootChanged: true | ||
} | ||
if (command instanceof HiddenCommand) { | ||
resolve({ | ||
...state, ...{ | ||
hiddenRoot: newResult as SModelRoot, | ||
hiddenRootChanged: true | ||
} | ||
}); | ||
} else if (command instanceof PopupCommand) { | ||
resolve({ | ||
...state, ...{ | ||
popupRoot: newResult as SModelRoot, | ||
popupChanged: true | ||
} | ||
}); | ||
} else if (newResult instanceof Promise) { | ||
newResult.then( | ||
(newModel: SModelRoot) => { | ||
beforeResolve.call(this, command, context); | ||
resolve({ | ||
...state, ...{ | ||
root: newModel, | ||
rootChanged: true | ||
} | ||
}); | ||
} | ||
); | ||
} else { | ||
}); | ||
} else if (command instanceof PopupCommand) { | ||
resolve({ | ||
...state, ...{ | ||
popupRoot: newResult as SModelRoot, | ||
popupChanged: true | ||
} | ||
}); | ||
} else if (newResult instanceof Promise) { | ||
newResult.then( | ||
(newModel: SModelRoot) => { | ||
beforeResolve.call(this, command, context); | ||
resolve({ | ||
...state, ...{ | ||
root: newResult, | ||
root: newModel, | ||
rootChanged: true | ||
@@ -240,5 +230,14 @@ } | ||
} | ||
); | ||
} else { | ||
beforeResolve.call(this, command, context); | ||
resolve({ | ||
...state, ...{ | ||
root: newResult, | ||
rootChanged: true | ||
} | ||
}); | ||
return promise; | ||
}); | ||
} | ||
}) | ||
); | ||
} | ||
@@ -256,21 +255,19 @@ | ||
*/ | ||
protected thenUpdate() { | ||
this.currentPromise = this.currentPromise.then( | ||
state => { | ||
if (state.hiddenRootChanged && state.hiddenRoot !== undefined) | ||
this.updateHidden(state.hiddenRoot); | ||
if (state.rootChanged) | ||
this.update(state.root); | ||
if (state.popupChanged && state.popupRoot !== undefined) | ||
this.updatePopup(state.popupRoot); | ||
return { | ||
root: state.root, | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}; | ||
} | ||
); | ||
protected thenUpdate(): Promise<SModelRoot> { | ||
this.currentPromise = this.currentPromise.then(async state => { | ||
if (state.hiddenRootChanged && state.hiddenRoot !== undefined) | ||
await this.updateHidden(state.hiddenRoot); | ||
if (state.rootChanged) | ||
await this.update(state.root); | ||
if (state.popupChanged && state.popupRoot !== undefined) | ||
await this.updatePopup(state.popupRoot); | ||
return { | ||
root: state.root, | ||
hiddenRoot: undefined, | ||
popupRoot: undefined, | ||
rootChanged: false, | ||
hiddenRootChanged: false, | ||
popupChanged: false | ||
}; | ||
}); | ||
return this.currentModel; | ||
@@ -282,39 +279,24 @@ } | ||
*/ | ||
updatePopup(model: SModelRoot): void { | ||
if (this.viewer) { | ||
this.viewer.updatePopup(model); | ||
return; | ||
} | ||
this.viewerProvider().then(viewer => { | ||
this.viewer = viewer; | ||
this.updatePopup(model); | ||
}); | ||
async update(model: SModelRoot): Promise<void> { | ||
if (this.viewer === undefined) | ||
this.viewer = await this.viewerProvider(); | ||
this.viewer.update(model); | ||
} | ||
/** | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
*/ | ||
update(model: SModelRoot): void { | ||
if (this.viewer) { | ||
this.viewer.update(model); | ||
return; | ||
} | ||
this.viewerProvider().then(viewer => { | ||
this.viewer = viewer; | ||
this.update(model); | ||
}); | ||
async updateHidden(model: SModelRoot): Promise<void> { | ||
if (this.viewer === undefined) | ||
this.viewer = await this.viewerProvider(); | ||
this.viewer.updateHidden(model); | ||
} | ||
/** | ||
* Notify the <code>Viewer</code> that the hidden model has changed. | ||
* Notify the <code>Viewer</code> that the model has changed. | ||
*/ | ||
updateHidden(model: SModelRoot): void { | ||
if (this.viewer) { | ||
this.viewer.updateHidden(model); | ||
return; | ||
} | ||
this.viewerProvider().then(viewer => { | ||
this.viewer = viewer; | ||
this.updateHidden(model); | ||
}); | ||
async updatePopup(model: SModelRoot): Promise<void> { | ||
if (this.viewer === undefined) | ||
this.viewer = await this.viewerProvider(); | ||
this.viewer.updatePopup(model); | ||
} | ||
@@ -333,3 +315,3 @@ | ||
*/ | ||
protected mergeOrPush(command: ICommand, context: CommandExecutionContext) { | ||
protected mergeOrPush(command: ICommand, context: CommandExecutionContext): void { | ||
if (command instanceof HiddenCommand) | ||
@@ -355,3 +337,3 @@ return; | ||
*/ | ||
protected undoOffStackSystemCommands() { | ||
protected undoOffStackSystemCommands(): void { | ||
let command = this.offStack.pop(); | ||
@@ -370,3 +352,3 @@ while (command !== undefined) { | ||
*/ | ||
protected undoPreceedingSystemCommands() { | ||
protected undoPreceedingSystemCommands(): void { | ||
let command = this.undoStack[this.undoStack.length - 1]; | ||
@@ -388,3 +370,3 @@ while (command !== undefined && command instanceof SystemCommand) { | ||
*/ | ||
protected redoFollowingSystemCommands() { | ||
protected redoFollowingSystemCommands(): void { | ||
let command = this.redoStack[this.redoStack.length - 1]; | ||
@@ -405,3 +387,3 @@ while (command !== undefined && command instanceof SystemCommand) { | ||
protected createContext(currentModel: SModelRoot): CommandExecutionContext { | ||
const context: CommandExecutionContext = { | ||
return { | ||
root: currentModel, | ||
@@ -414,3 +396,2 @@ modelChanged: this, | ||
}; | ||
return context; | ||
} | ||
@@ -417,0 +398,0 @@ } |
@@ -34,3 +34,7 @@ /* | ||
export interface ICommand { | ||
readonly blockUntilActionKind?: string | ||
/** | ||
* If this property is present, all following actions are blocked | ||
* until the function returns `true`. | ||
*/ | ||
readonly blockUntil?: (action: Action) => boolean; | ||
@@ -37,0 +41,0 @@ execute(context: CommandExecutionContext): CommandResult |
@@ -62,5 +62,5 @@ /* | ||
get blockUntilActionKind() { | ||
return InitializeCanvasBoundsCommand.KIND; | ||
get blockUntil(): (action: Action) => boolean { | ||
return action => action.kind === InitializeCanvasBoundsCommand.KIND; | ||
} | ||
} |
@@ -28,2 +28,3 @@ /* | ||
IModelFactory: Symbol('IModelFactory'), | ||
IModelLayoutEngine: Symbol('IModelLayoutEngine'), | ||
ModelRendererFactory: Symbol('ModelRendererFactory'), | ||
@@ -33,3 +34,7 @@ ModelSource: Symbol('ModelSource'), | ||
MouseListener: Symbol('MouseListener'), | ||
/** | ||
* @deprecated Use IPopupModelProvider instead. | ||
*/ | ||
PopupModelFactory: Symbol('PopupModelFactory'), | ||
IPopupModelProvider: Symbol('IPopupModelProvider'), | ||
PopupMouseListener: Symbol('PopupMouseListener'), | ||
@@ -36,0 +41,0 @@ PopupVNodeDecorator: Symbol('PopupVNodeDecorator'), |
@@ -135,5 +135,5 @@ /* | ||
get blockUntilActionKind() { | ||
return ComputedBoundsAction.KIND; | ||
get blockUntil(): (action: Action) => boolean { | ||
return action => action.kind === ComputedBoundsAction.KIND; | ||
} | ||
} |
@@ -137,15 +137,7 @@ /* | ||
undo(context: CommandExecutionContext) { | ||
if (this.action.animate) { | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start(); | ||
} else { | ||
return this.doMove(context, true); | ||
} | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start(); | ||
} | ||
redo(context: CommandExecutionContext) { | ||
if (this.action.animate) { | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start(); | ||
} else { | ||
return this.doMove(context, false); | ||
} | ||
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start(); | ||
} | ||
@@ -152,0 +144,0 @@ |
@@ -8,2 +8,3 @@ /* | ||
import { injectable } from "inversify"; | ||
import { center, maxDistance, Point } from "../utils/geometry"; | ||
@@ -17,44 +18,52 @@ import { SEdge } from "./sgraph"; | ||
export type EdgeRouter = (edge: SEdge) => RoutedPoint[]; | ||
export interface IEdgeRouter { | ||
route(edge: SEdge): RoutedPoint[] | ||
} | ||
const minimalPointDistance: number = 2; | ||
export function linearRoute(edge: SEdge): RoutedPoint[] { | ||
const source = edge.source; | ||
const target = edge.target; | ||
if (source === undefined || target === undefined) { | ||
return []; | ||
} | ||
export interface LinearRouteOptions { | ||
minimalPointDistance: number; | ||
} | ||
let sourceAnchor: Point; | ||
let targetAnchor: Point; | ||
const rpCount = edge.routingPoints !== undefined ? edge.routingPoints.length : 0; | ||
if (rpCount >= 1) { | ||
// Use the first routing point as start anchor reference | ||
const p0 = edge.routingPoints[0]; | ||
sourceAnchor = source.getTranslatedAnchor(p0, edge.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the last routing point as end anchor reference | ||
const pn = edge.routingPoints[rpCount - 1]; | ||
targetAnchor = target.getTranslatedAnchor(pn, edge.parent, edge, edge.targetAnchorCorrection); | ||
} else { | ||
// Use the target center as start anchor reference | ||
const startRef = center(target.bounds); | ||
sourceAnchor = source.getTranslatedAnchor(startRef, target.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the source center as end anchor reference | ||
const endRef = center(source.bounds); | ||
targetAnchor = target.getTranslatedAnchor(endRef, source.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
@injectable() | ||
export class LinearEdgeRouter implements IEdgeRouter { | ||
route(edge: SEdge, options: LinearRouteOptions = { minimalPointDistance: 2 }): RoutedPoint[] { | ||
const source = edge.source; | ||
const target = edge.target; | ||
if (source === undefined || target === undefined) { | ||
return []; | ||
} | ||
const result: RoutedPoint[] = []; | ||
result.push({ kind: 'source', x: sourceAnchor.x, y: sourceAnchor.y }); | ||
for (let i = 0; i < rpCount; i++) { | ||
const p = edge.routingPoints[i]; | ||
if (i > 0 && i < rpCount - 1 | ||
|| i === 0 && maxDistance(sourceAnchor, p) >= minimalPointDistance + (edge.sourceAnchorCorrection || 0) | ||
|| i === rpCount - 1 && maxDistance(p, targetAnchor) >= minimalPointDistance + (edge.targetAnchorCorrection || 0)) { | ||
result.push({ kind: 'linear', x: p.x, y: p.y, pointIndex: i }); | ||
let sourceAnchor: Point; | ||
let targetAnchor: Point; | ||
const rpCount = edge.routingPoints !== undefined ? edge.routingPoints.length : 0; | ||
if (rpCount >= 1) { | ||
// Use the first routing point as start anchor reference | ||
const p0 = edge.routingPoints[0]; | ||
sourceAnchor = source.getTranslatedAnchor(p0, edge.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the last routing point as end anchor reference | ||
const pn = edge.routingPoints[rpCount - 1]; | ||
targetAnchor = target.getTranslatedAnchor(pn, edge.parent, edge, edge.targetAnchorCorrection); | ||
} else { | ||
// Use the target center as start anchor reference | ||
const startRef = center(target.bounds); | ||
sourceAnchor = source.getTranslatedAnchor(startRef, target.parent, edge, edge.sourceAnchorCorrection); | ||
// Use the source center as end anchor reference | ||
const endRef = center(source.bounds); | ||
targetAnchor = target.getTranslatedAnchor(endRef, source.parent, edge, edge.targetAnchorCorrection); | ||
} | ||
const result: RoutedPoint[] = []; | ||
result.push({ kind: 'source', x: sourceAnchor.x, y: sourceAnchor.y }); | ||
for (let i = 0; i < rpCount; i++) { | ||
const p = edge.routingPoints[i]; | ||
if (i > 0 && i < rpCount - 1 | ||
|| i === 0 && maxDistance(sourceAnchor, p) >= options.minimalPointDistance + (edge.sourceAnchorCorrection || 0) | ||
|| i === rpCount - 1 && maxDistance(p, targetAnchor) >= options.minimalPointDistance + (edge.targetAnchorCorrection || 0)) { | ||
result.push({ kind: 'linear', x: p.x, y: p.y, pointIndex: i }); | ||
} | ||
} | ||
result.push({ kind: 'target', x: targetAnchor.x, y: targetAnchor.y}); | ||
return result; | ||
} | ||
result.push({ kind: 'target', x: targetAnchor.x, y: targetAnchor.y}); | ||
return result; | ||
} |
@@ -24,3 +24,3 @@ /* | ||
import { translatePoint } from '../base/model/smodel-utils'; | ||
import { RoutedPoint, linearRoute } from './routing'; | ||
import { RoutedPoint, LinearEdgeRouter, IEdgeRouter } from './routing'; | ||
@@ -182,2 +182,3 @@ /** | ||
targetAnchorCorrection?: number; | ||
router?: IEdgeRouter; | ||
@@ -193,3 +194,5 @@ get source(): SConnectableElement | undefined { | ||
route(): RoutedPoint[] { | ||
const route = linearRoute(this); | ||
if (this.router === undefined) | ||
this.router = new LinearEdgeRouter(); | ||
const route = this.router.route(this); | ||
return filterEditModeHandles(route, this); | ||
@@ -196,0 +199,0 @@ } |
@@ -117,3 +117,5 @@ /* | ||
this.logger.log(this, 'receiving', object); | ||
this.actionDispatcher.dispatch(object.action, this.storeNewModel.bind(this)); | ||
this.actionDispatcher.dispatch(object.action).then(() => { | ||
this.storeNewModel(object.action); | ||
}); | ||
} | ||
@@ -120,0 +122,0 @@ } else { |
@@ -29,10 +29,12 @@ /* | ||
dispatchAll(actions: Action[]): void { | ||
dispatchAll(actions: Action[]): Promise<void> { | ||
for (const action of actions) { | ||
this.dispatch(action); | ||
} | ||
return Promise.resolve(); | ||
} | ||
dispatch(action: Action): void { | ||
dispatch(action: Action): Promise<void> { | ||
this.actions.push(action); | ||
return Promise.resolve(); | ||
} | ||
@@ -151,2 +153,3 @@ } | ||
modelSource.setModel({ type: 'root', id: 'root' }); | ||
modelSource.addElements([ | ||
@@ -163,4 +166,4 @@ { | ||
expect(modelSource.model).to.deep.equal({ | ||
type: 'NONE', | ||
id: 'ROOT', | ||
type: 'root', | ||
id: 'root', | ||
children: [ | ||
@@ -179,4 +182,4 @@ { | ||
expect(modelSource.model).to.deep.equal({ | ||
type: 'NONE', | ||
id: 'ROOT', | ||
type: 'root', | ||
id: 'root', | ||
children: [ | ||
@@ -190,5 +193,5 @@ { | ||
expect(dispatcher.actions).to.have.lengthOf(2); | ||
const action0 = dispatcher.actions[0] as UpdateModelAction; | ||
expect(action0.matches).to.deep.equal([ | ||
expect(dispatcher.actions).to.have.lengthOf(3); | ||
const action1 = dispatcher.actions[1] as UpdateModelAction; | ||
expect(action1.matches).to.deep.equal([ | ||
{ | ||
@@ -199,3 +202,3 @@ right: { | ||
}, | ||
rightParentId: 'ROOT' | ||
rightParentId: 'root' | ||
}, | ||
@@ -207,7 +210,7 @@ { | ||
}, | ||
rightParentId: 'ROOT' | ||
rightParentId: 'root' | ||
} | ||
]); | ||
const action1 = dispatcher.actions[1] as UpdateModelAction; | ||
expect(action1.matches).to.deep.equal([ | ||
const action2 = dispatcher.actions[2] as UpdateModelAction; | ||
expect(action2.matches).to.deep.equal([ | ||
{ | ||
@@ -218,6 +221,57 @@ left: { | ||
}, | ||
leftParentId: 'ROOT' | ||
leftParentId: 'root' | ||
} | ||
]); | ||
}); | ||
it('resolves promises in fixed mode', async () => { | ||
const container = setup({ needsClientLayout: false }); | ||
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource); | ||
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher); | ||
const root1: SModelRootSchema = { | ||
type: 'root', | ||
id: 'root' | ||
}; | ||
await modelSource.setModel(root1); | ||
const root2: SModelRootSchema = { | ||
type: 'root', | ||
id: 'root', | ||
children: [{ type: 'blob', id: 'foo' }] | ||
}; | ||
await modelSource.updateModel(root2); | ||
expect(dispatcher.actions).to.have.lengthOf(2); | ||
}); | ||
it('resolves promises in dynamic mode', async () => { | ||
const container = setup({ needsClientLayout: true }); | ||
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource); | ||
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher); | ||
const root1: SModelRootSchema = { | ||
type: 'root', | ||
id: 'root', | ||
children: [{ type: 'node', id: 'child1' }] | ||
}; | ||
const promise1 = modelSource.setModel(root1); | ||
modelSource.handle(new ComputedBoundsAction([ | ||
{ elementId: 'child1', newBounds: { x: 10, y: 10, width: 20, height: 20 } } | ||
])); | ||
await promise1; | ||
const root2: SModelRootSchema = { | ||
type: 'root', | ||
id: 'root', | ||
children: [{ type: 'node', id: 'bar' }] | ||
}; | ||
const promise2 = modelSource.updateModel(root2); | ||
modelSource.handle(new ComputedBoundsAction([ | ||
{ elementId: 'bar', newBounds: { x: 10, y: 10, width: 20, height: 20 } } | ||
])); | ||
await promise2; | ||
expect(dispatcher.actions).to.have.lengthOf(4); | ||
}); | ||
}); |
@@ -10,2 +10,4 @@ /* | ||
import { Bounds, Point } from "../utils/geometry"; | ||
import { Deferred } from "../utils/async"; | ||
import { ILogger } from "../utils/logging"; | ||
import { TYPES } from "../base/types"; | ||
@@ -29,12 +31,6 @@ import { Action } from "../base/actions/action"; | ||
export type PopupModelFactory = (request: RequestPopupModelAction, element?: SModelElementSchema) | ||
=> SModelRootSchema | undefined; | ||
export interface IStateAwareModelProvider { | ||
getModel(diagramState: DiagramState, currentRoot?: SModelRootSchema): SModelRootSchema | ||
} | ||
/** | ||
* A model source that handles actions for bounds calculation and model | ||
* updates. | ||
* A model source that allows to set and modify the model through function calls. | ||
* This class can be used as a facade over the action-based API of sprotty. It handles | ||
* actions for bounds calculation and model updates. | ||
*/ | ||
@@ -49,4 +45,20 @@ @injectable() | ||
protected diagramState: DiagramState = { | ||
expansionState: new ExpansionState(this.currentRoot) | ||
}; | ||
/** | ||
* The `type` property of the model root is used to determine whether a model update | ||
* is a change of the previous model or a totally new one. | ||
*/ | ||
protected lastSubmittedModelType: string; | ||
/** | ||
* When client layout is active, model updates are not applied immediately. Instead the | ||
* model is rendered on a hidden canvas first to derive actual bounds. The promises listed | ||
* here are resolved after the new bounds have been applied and the new model state has | ||
* been actually applied to the visible canvas. | ||
*/ | ||
protected pendingUpdates: Deferred<void>[] = []; | ||
get model(): SModelRootSchema { | ||
@@ -60,13 +72,9 @@ return this.currentRoot; | ||
protected onModelSubmitted: (newRoot: SModelRootSchema) => void; | ||
protected diagramState: DiagramState = { | ||
expansionState: new ExpansionState(this.currentRoot) | ||
}; | ||
constructor(@inject(TYPES.IActionDispatcher) actionDispatcher: IActionDispatcher, | ||
@inject(TYPES.ActionHandlerRegistry) actionHandlerRegistry: ActionHandlerRegistry, | ||
@inject(TYPES.ViewerOptions) viewerOptions: ViewerOptions, | ||
@inject(TYPES.PopupModelFactory)@optional() protected popupModelFactory?: PopupModelFactory, | ||
@inject(TYPES.ILogger) protected readonly logger: ILogger, | ||
@inject(TYPES.StateAwareModelProvider)@optional() protected modelProvider?: IStateAwareModelProvider, | ||
@inject(TYPES.IPopupModelProvider)@optional() protected popupModelProvider?: IPopupModelProvider, | ||
@inject(TYPES.IModelLayoutEngine)@optional() protected layoutEngine?: IModelLayoutEngine | ||
) { | ||
@@ -89,3 +97,6 @@ super(actionDispatcher, actionHandlerRegistry, viewerOptions); | ||
setModel(newRoot: SModelRootSchema): void { | ||
/** | ||
* Set the model without incremental update. | ||
*/ | ||
setModel(newRoot: SModelRootSchema): Promise<void> { | ||
this.currentRoot = newRoot; | ||
@@ -95,50 +106,77 @@ this.diagramState = { | ||
}; | ||
this.submitModel(newRoot, false); | ||
return this.submitModel(newRoot, false); | ||
} | ||
updateModel(newRoot?: SModelRootSchema): void { | ||
/** | ||
* Apply an incremental update to the model with an animation showing the transition to | ||
* the new state. If `newRoot` is undefined, the current root is submitted; in that case | ||
* it is assumed that it has been modified before. | ||
*/ | ||
updateModel(newRoot?: SModelRootSchema): Promise<void> { | ||
if (newRoot === undefined) { | ||
this.submitModel(this.currentRoot, true); | ||
return this.submitModel(this.currentRoot, true); | ||
} else { | ||
this.currentRoot = newRoot; | ||
this.submitModel(newRoot, true); | ||
return this.submitModel(newRoot, true); | ||
} | ||
} | ||
protected submitModel(newRoot: SModelRootSchema, update: boolean): void { | ||
/** | ||
* If client layout is active, run a `RequestBoundsAction` and wait for the resulting | ||
* `ComputedBoundsAction`, otherwise call `doSubmitModel(…)` directly. | ||
*/ | ||
protected submitModel(newRoot: SModelRootSchema, update: boolean | Match[]): Promise<void> { | ||
if (this.viewerOptions.needsClientLayout) { | ||
const deferred = new Deferred<void>(); | ||
this.pendingUpdates.push(deferred); | ||
this.actionDispatcher.dispatch(new RequestBoundsAction(newRoot)); | ||
return deferred.promise; | ||
} else { | ||
this.doSubmitModel(newRoot, update); | ||
return this.doSubmitModel(newRoot, update); | ||
} | ||
} | ||
protected doSubmitModel(newRoot: SModelRootSchema, update: boolean): void { | ||
if (update && newRoot.type === this.lastSubmittedModelType) { | ||
this.actionDispatcher.dispatch(new UpdateModelAction(newRoot)); | ||
} else { | ||
this.actionDispatcher.dispatch(new SetModelAction(newRoot)); | ||
/** | ||
* Submit the given model with an `UpdateModelAction` or a `SetModelAction` depending on the | ||
* `update` argument. If available, the model layout engine is invoked first. | ||
*/ | ||
protected async doSubmitModel(newRoot: SModelRootSchema, update: boolean | Match[], index?: SModelIndex<SModelElementSchema>): Promise<void> { | ||
if (this.layoutEngine !== undefined) { | ||
try { | ||
const layoutResult = this.layoutEngine.layout(newRoot, index); | ||
if (layoutResult instanceof Promise) | ||
newRoot = await layoutResult; | ||
else if (layoutResult !== undefined) | ||
newRoot = layoutResult; | ||
} catch (error) { | ||
this.logger.error(this, error.toString(), error.stack); | ||
} | ||
} | ||
const lastSubmittedModelType = this.lastSubmittedModelType; | ||
this.lastSubmittedModelType = newRoot.type; | ||
if (this.onModelSubmitted !== undefined) { | ||
this.onModelSubmitted(newRoot); | ||
const updates = this.pendingUpdates; | ||
this.pendingUpdates = []; | ||
if (update && newRoot.type === lastSubmittedModelType) { | ||
const input = Array.isArray(update) ? update : newRoot; | ||
await this.actionDispatcher.dispatch(new UpdateModelAction(input)); | ||
} else { | ||
await this.actionDispatcher.dispatch(new SetModelAction(newRoot)); | ||
} | ||
updates.forEach(d => d.resolve()); | ||
} | ||
applyMatches(matches: Match[]): void { | ||
/** | ||
* Modify the current model with an array of matches. | ||
*/ | ||
applyMatches(matches: Match[]): Promise<void> { | ||
const root = this.currentRoot; | ||
applyMatches(root, matches); | ||
if (this.viewerOptions.needsClientLayout) { | ||
this.actionDispatcher.dispatch(new RequestBoundsAction(root)); | ||
} else { | ||
const update = new UpdateModelAction(matches); | ||
this.actionDispatcher.dispatch(update); | ||
this.lastSubmittedModelType = root.type; | ||
if (this.onModelSubmitted !== undefined) { | ||
this.onModelSubmitted(root); | ||
} | ||
} | ||
return this.submitModel(root, matches); | ||
} | ||
addElements(elements: (SModelElementSchema | { element: SModelElementSchema, parentId: string })[]): void { | ||
/** | ||
* Modify the current model by adding new elements. | ||
*/ | ||
addElements(elements: (SModelElementSchema | { element: SModelElementSchema, parentId: string })[]): Promise<void> { | ||
const matches: Match[] = []; | ||
@@ -159,6 +197,9 @@ for (const e of elements) { | ||
} | ||
this.applyMatches(matches); | ||
return this.applyMatches(matches); | ||
} | ||
removeElements(elements: (string | { elementId: string, parentId: string })[]) { | ||
/** | ||
* Modify the current model by removing elements. | ||
*/ | ||
removeElements(elements: (string | { elementId: string, parentId: string })[]): Promise<void> { | ||
const matches: Match[] = []; | ||
@@ -187,5 +228,8 @@ const index = new SModelIndex(); | ||
} | ||
this.applyMatches(matches); | ||
return this.applyMatches(matches); | ||
} | ||
// ----- Methods for handling incoming actions ---------------------------- | ||
handle(action: Action): void { | ||
@@ -236,3 +280,3 @@ switch (action.kind) { | ||
} | ||
this.doSubmitModel(root, true); | ||
this.doSubmitModel(root, true, index); | ||
} | ||
@@ -252,5 +296,5 @@ | ||
protected handleRequestPopupModel(action: RequestPopupModelAction): void { | ||
if (this.popupModelFactory !== undefined) { | ||
if (this.popupModelProvider !== undefined) { | ||
const element = findElement(this.currentRoot, action.elementId); | ||
const popupRoot = this.popupModelFactory(action, element); | ||
const popupRoot = this.popupModelProvider.getPopupModel(action, element); | ||
if (popupRoot !== undefined) { | ||
@@ -288,1 +332,19 @@ popupRoot.canvasBounds = action.bounds; | ||
} | ||
/** | ||
* @deprecated Use IPopupModelProvider instead. | ||
*/ | ||
export type PopupModelFactory = (request: RequestPopupModelAction, element?: SModelElementSchema) | ||
=> SModelRootSchema | undefined; | ||
export interface IPopupModelProvider { | ||
getPopupModel(request: RequestPopupModelAction, element?: SModelElementSchema): SModelRootSchema | undefined; | ||
} | ||
export interface IStateAwareModelProvider { | ||
getModel(diagramState: DiagramState, currentRoot?: SModelRootSchema): SModelRootSchema | ||
} | ||
export interface IModelLayoutEngine { | ||
layout(model: SModelRootSchema, index?: SModelIndex<SModelElementSchema>): SModelRootSchema | Promise<SModelRootSchema> | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1348486
458
24890