sprotty
Version:
A next-gen framework for graphical views
503 lines • 24.9 kB
JavaScript
"use strict";
/********************************************************************************
* 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
********************************************************************************/
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;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SBezierControlHandleView = exports.SBezierCreateHandleView = exports.SCompartmentView = exports.SLabelView = exports.SRoutingHandleView = exports.BezierCurveEdgeView = exports.PolylineEdgeViewWithGapsOnIntersections = exports.JumpingPolylineEdgeView = exports.PolylineEdgeView = exports.SGraphView = void 0;
/** @jsx svg */
const inversify_1 = require("inversify");
const geometry_1 = require("sprotty-protocol/lib/utils/geometry");
const model_utils_1 = require("sprotty-protocol/lib/utils/model-utils");
const vnode_utils_1 = require("../base/views/vnode-utils");
const views_1 = require("../features/bounds/views");
const intersection_finder_1 = require("../features/edge-intersection/intersection-finder");
const model_1 = require("../features/edge-layout/model");
const model_2 = require("../features/routing/model");
const routing_1 = require("../features/routing/routing");
const views_2 = require("../features/routing/views");
const jsx_1 = require("../lib/jsx");
const geometry_2 = require("../utils/geometry");
/**
* IView component that turns an SGraph element and its children into a tree of virtual DOM elements.
*/
let SGraphView = class SGraphView {
render(model, context) {
const edgeRouting = this.edgeRouterRegistry.routeAllChildren(model);
const transform = `scale(${model.zoom}) translate(${-model.scroll.x},${-model.scroll.y})`;
return (0, jsx_1.svg)("svg", { "class-sprotty-graph": true },
(0, jsx_1.svg)("g", { transform: transform }, context.renderChildren(model, { edgeRouting })));
}
};
exports.SGraphView = SGraphView;
__decorate([
(0, inversify_1.inject)(routing_1.EdgeRouterRegistry),
__metadata("design:type", routing_1.EdgeRouterRegistry)
], SGraphView.prototype, "edgeRouterRegistry", void 0);
exports.SGraphView = SGraphView = __decorate([
(0, inversify_1.injectable)()
], SGraphView);
let PolylineEdgeView = class PolylineEdgeView extends views_2.RoutableView {
render(edge, context, args) {
const route = this.edgeRouterRegistry.route(edge, args);
if (route.length === 0) {
return this.renderDanglingEdge("Cannot compute route", edge, context);
}
if (!this.isVisible(edge, route, context)) {
if (edge.children.length === 0) {
return undefined;
}
// The children of an edge are not necessarily inside the bounding box of the route,
// so we need to render a group to ensure the children have a chance to be rendered.
return (0, jsx_1.svg)("g", null, context.renderChildren(edge, { route }));
}
return (0, jsx_1.svg)("g", { "class-sprotty-edge": true, "class-mouseover": edge.hoverFeedback },
this.renderLine(edge, route, context, args),
this.renderAdditionals(edge, route, context),
this.renderJunctionPoints(edge, route, context, args),
context.renderChildren(edge, { route }));
}
renderJunctionPoints(edge, route, context, args) {
const radius = 5;
const junctionPoints = [];
for (let i = 1; i < route.length; i++) {
if (route[i].isJunction) {
junctionPoints.push((0, jsx_1.svg)("circle", { cx: route[i].x, cy: route[i].y, r: radius }));
}
}
if (junctionPoints.length > 0) {
return (0, jsx_1.svg)("g", { "class-sprotty-junction": true }, junctionPoints);
}
return undefined;
}
renderLine(edge, segments, context, args) {
const firstPoint = segments[0];
let path = `M ${firstPoint.x},${firstPoint.y}`;
for (let i = 1; i < segments.length; i++) {
const p = segments[i];
path += ` L ${p.x},${p.y}`;
}
return (0, jsx_1.svg)("path", { d: path });
}
renderAdditionals(edge, segments, context) {
// here we need to render the control points?
return [];
}
renderDanglingEdge(message, edge, context) {
return (0, jsx_1.svg)("text", { "class-sprotty-edge-dangling": true, title: message }, "?");
}
};
exports.PolylineEdgeView = PolylineEdgeView;
__decorate([
(0, inversify_1.inject)(routing_1.EdgeRouterRegistry),
__metadata("design:type", routing_1.EdgeRouterRegistry)
], PolylineEdgeView.prototype, "edgeRouterRegistry", void 0);
exports.PolylineEdgeView = PolylineEdgeView = __decorate([
(0, inversify_1.injectable)()
], PolylineEdgeView);
/**
* A `PolylineEdgeView` that renders jumps over intersections.
*
* In order to find intersections, `IntersectionFinder` needs to be configured as a `TYPES.IEdgeRoutePostprocessor`
* so that that intersections are declared as `IntersectingRoutedPoint` in the computed routes.
*
* This view only draws correct line jumps for intersections among straight line segments and doesn't work with bezier curves.
*
* @see IntersectionFinder
* @see IntersectingRoutedPoint
* @see EdgeRouterRegistry
*/
let JumpingPolylineEdgeView = class JumpingPolylineEdgeView extends PolylineEdgeView {
constructor() {
super(...arguments);
this.jumpOffsetBefore = 5;
this.jumpOffsetAfter = 5;
this.skipOffsetBefore = 3;
this.skipOffsetAfter = 2;
}
renderLine(edge, segments, context, args) {
let path = '';
for (let i = 0; i < segments.length; i++) {
const p = segments[i];
if (i === 0) {
path = `M ${p.x},${p.y}`;
}
if ((0, intersection_finder_1.isIntersectingRoutedPoint)(p)) {
path += this.intersectionPath(edge, segments, p, args);
}
if (i !== 0) {
path += ` L ${p.x},${p.y}`;
}
}
return (0, jsx_1.svg)("path", { d: path });
}
/**
* Returns a path that takes the intersections into account by drawing a line jump or a gap for intersections on that path.
*/
intersectionPath(edge, segments, intersectingPoint, args) {
if (intersectingPoint.intersections.length < 1) {
return '';
}
const segment = this.getLineSegment(edge, intersectingPoint.intersections[0], args, segments);
const intersections = this.getIntersectionsSortedBySegmentDirection(segment, intersectingPoint);
let path = '';
for (const intersection of intersections) {
const otherLineSegment = this.getOtherLineSegment(edge, intersection, args);
if (otherLineSegment === undefined) {
continue;
}
const currentLineSegment = this.getLineSegment(edge, intersection, args, segments);
const intersectionPoint = intersection.intersectionPoint;
if (this.shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment)) {
path += this.createJumpPath(intersectionPoint, currentLineSegment);
}
else if (this.shouldDrawLineGapOnIntersection(currentLineSegment, otherLineSegment)) {
path += this.createGapPath(intersectionPoint, currentLineSegment);
}
}
return path;
}
/**
* Returns the intersections sorted by the direction of the `lineSegment`.
*
* The coordinate system goes from left to right and top to bottom.
* Thus, x increases to the right and y increases downwards.
*
* We need to draw the intersections in the order of the direction of the line segment.
* To draw a line pointing north, we need to order intersections by Y in a descending order.
* To draw a line pointing south, we need to order intersections by Y in an ascending order.
*/
getIntersectionsSortedBySegmentDirection(lineSegment, intersectingPoint) {
switch (lineSegment.direction) {
case 'north':
case 'north-east':
return intersectingPoint.intersections.sort(intersection_finder_1.BY_X_THEN_DESCENDING_Y);
case 'south':
case 'south-east':
case 'east':
return intersectingPoint.intersections.sort(intersection_finder_1.BY_X_THEN_Y);
case 'south-west':
case 'west':
return intersectingPoint.intersections.sort(intersection_finder_1.BY_DESCENDING_X_THEN_Y);
case 'north-west':
return intersectingPoint.intersections.sort(intersection_finder_1.BY_DESCENDING_X_THEN_DESCENDING_Y);
}
}
/**
* Whether or not to draw a line jump on an intersection for the `currentLineSegment`.
* This should usually be inverse of `shouldDrawLineGapOnIntersection()`.
*/
shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment) {
return Math.abs(currentLineSegment.slopeOrMax) < Math.abs(otherLineSegment.slopeOrMax);
}
/**
* Whether or not to draw a line gap on an intersection for the `currentLineSegment`.
* This should usually be inverse of `shouldDrawLineJumpOnIntersection()`.
*/
shouldDrawLineGapOnIntersection(currentLineSegment, otherLineSegment) {
return !this.shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment);
}
getLineSegment(edge, intersection, args, segments) {
const route = segments ? segments : this.edgeRouterRegistry.route(edge, args);
const index = intersection.routable1 === edge.id ? intersection.segmentIndex1 : intersection.segmentIndex2;
return new geometry_2.PointToPointLine(route[index], route[index + 1]);
}
getOtherLineSegment(currentEdge, intersection, args) {
const otherEdgeId = intersection.routable1 === currentEdge.id ? intersection.routable2 : intersection.routable1;
const otherEdge = currentEdge.index.getById(otherEdgeId);
if (!(otherEdge instanceof model_2.SRoutableElementImpl)) {
return undefined;
}
return this.getLineSegment(otherEdge, intersection, args);
}
createJumpPath(intersectionPoint, lineSegment) {
const anchorBefore = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p1, this.jumpOffsetBefore);
const anchorAfter = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p2, this.jumpOffsetAfter);
const rotation = lineSegment.p1.x < lineSegment.p2.x ? 1 : 0;
return ` L ${anchorBefore.x},${anchorBefore.y} A 1,1 0,0 ${rotation} ${anchorAfter.x},${anchorAfter.y}`;
}
createGapPath(intersectionPoint, lineSegment) {
let offsetBefore;
let offsetAfter;
if (intersectionPoint.y < lineSegment.p1.y) {
offsetBefore = -this.skipOffsetBefore;
offsetAfter = this.jumpOffsetAfter + this.skipOffsetAfter;
}
else {
offsetBefore = this.jumpOffsetBefore + this.skipOffsetAfter;
offsetAfter = -this.skipOffsetBefore;
}
const anchorBefore = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p1, offsetBefore);
const anchorAfter = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p2, offsetAfter);
return ` L ${anchorBefore.x},${anchorBefore.y} M ${anchorAfter.x},${anchorAfter.y}`;
}
};
exports.JumpingPolylineEdgeView = JumpingPolylineEdgeView;
exports.JumpingPolylineEdgeView = JumpingPolylineEdgeView = __decorate([
(0, inversify_1.injectable)()
], JumpingPolylineEdgeView);
/**
* A `PolylineEdgeView` that renders gaps on intersections.
*
* In order to find intersections, `IntersectionFinder` needs to be configured as a `TYPES.IEdgeRoutePostprocessor`
* so that that intersections are declared as `IntersectingRoutedPoint` in the computed routes.
*
* This view only draws correct gaps for intersections among straight line segments and doesn't work with bezier curves.
*
* @see IntersectionFinder
* @see IntersectingRoutedPoint
* @see EdgeRouterRegistry
*/
let PolylineEdgeViewWithGapsOnIntersections = class PolylineEdgeViewWithGapsOnIntersections extends JumpingPolylineEdgeView {
constructor() {
super(...arguments);
this.skipOffsetBefore = 3;
this.skipOffsetAfter = 3;
}
shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment) {
return false;
}
shouldDrawLineGapOnIntersection(currentLineSegment, otherLineSegment) {
return Math.abs(currentLineSegment.slopeOrMax) >= Math.abs(otherLineSegment.slopeOrMax);
}
createGapPath(intersectionPoint, lineSegment) {
const anchorBefore = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p1, this.skipOffsetBefore);
const anchorAfter = geometry_1.Point.shiftTowards(intersectionPoint, lineSegment.p2, this.skipOffsetAfter);
return ` L ${anchorBefore.x},${anchorBefore.y} M ${anchorAfter.x},${anchorAfter.y}`;
}
};
exports.PolylineEdgeViewWithGapsOnIntersections = PolylineEdgeViewWithGapsOnIntersections;
exports.PolylineEdgeViewWithGapsOnIntersections = PolylineEdgeViewWithGapsOnIntersections = __decorate([
(0, inversify_1.injectable)()
], PolylineEdgeViewWithGapsOnIntersections);
let BezierCurveEdgeView = class BezierCurveEdgeView extends views_2.RoutableView {
render(edge, context, args) {
const route = this.edgeRouterRegistry.route(edge, args);
if (route.length === 0) {
return this.renderDanglingEdge("Cannot compute route", edge, context);
}
if (!this.isVisible(edge, route, context)) {
if (edge.children.length === 0) {
return undefined;
}
// The children of an edge are not necessarily inside the bounding box of the route,
// so we need to render a group to ensure the children have a chance to be rendered.
return (0, jsx_1.svg)("g", null, context.renderChildren(edge, { route }));
}
return (0, jsx_1.svg)("g", { "class-sprotty-edge": true, "class-mouseover": edge.hoverFeedback },
this.renderLine(edge, route, context, args),
this.renderAdditionals(edge, route, context),
context.renderChildren(edge, { route }));
}
renderLine(edge, segments, context, args) {
/**
* Example for two splines:
* SVG:
* <path d="M0,300 C0,150 300,150 300,300 S600,450 600,300" />
*
* Segments input layout:
* routingPoints[0] = source;
* routingPoints[1] = controlForSource;
* routingPoints[2] = controlForSegment1;
* routingPoints[3] = segment;
* routingPoints[4] = controlForSegment2;
* routingPoints[5] = controlForTarget;
* routingPoints[6] = target;
*/
let path = '';
if (segments.length >= 4) {
path += this.buildMainSegment(segments);
const pointsLeft = segments.length - 4;
if (pointsLeft > 0 && pointsLeft % 3 === 0) {
for (let i = 4; i < segments.length; i += 3) {
path += this.addSpline(segments, i);
}
}
}
return (0, jsx_1.svg)("path", { d: path });
}
buildMainSegment(segments) {
const s = segments[0];
const h1 = segments[1];
const h2 = segments[2];
const t = segments[3];
return `M${s.x},${s.y} C${h1.x},${h1.y} ${h2.x},${h2.y} ${t.x},${t.y}`;
}
addSpline(segments, index) {
// We have two controls for each junction, but SVG does not therefore index is jumped over
const c = segments[index + 1];
const p = segments[index + 2];
return ` S${c.x},${c.y} ${p.x},${p.y}`;
}
renderAdditionals(edge, segments, context) {
return [];
}
renderDanglingEdge(message, edge, context) {
return (0, jsx_1.svg)("text", { "class-sprotty-edge-dangling": true, title: message }, "?");
}
};
exports.BezierCurveEdgeView = BezierCurveEdgeView;
__decorate([
(0, inversify_1.inject)(routing_1.EdgeRouterRegistry),
__metadata("design:type", routing_1.EdgeRouterRegistry)
], BezierCurveEdgeView.prototype, "edgeRouterRegistry", void 0);
exports.BezierCurveEdgeView = BezierCurveEdgeView = __decorate([
(0, inversify_1.injectable)()
], BezierCurveEdgeView);
let SRoutingHandleView = class SRoutingHandleView {
constructor() {
this.minimalPointDistance = 10;
}
render(handle, context, args) {
if (args && args.route) {
if (handle.parent instanceof model_2.SRoutableElementImpl) {
const router = this.edgeRouterRegistry.get(handle.parent.routerKind);
const theRoute = args.route === undefined ? this.edgeRouterRegistry.route(handle.parent, args) : args.route;
const position = router.getHandlePosition(handle.parent, theRoute, handle);
if (position !== undefined) {
const node = (0, jsx_1.svg)("circle", { "class-sprotty-routing-handle": true, "class-selected": handle.selected, "class-mouseover": handle.hoverFeedback, cx: position.x, cy: position.y, r: this.getRadius() });
(0, vnode_utils_1.setAttr)(node, 'data-kind', handle.kind);
return node;
}
}
}
// Fallback: Create an empty group
return (0, jsx_1.svg)("g", null);
}
getRadius() {
return 7;
}
};
exports.SRoutingHandleView = SRoutingHandleView;
__decorate([
(0, inversify_1.inject)(routing_1.EdgeRouterRegistry),
__metadata("design:type", routing_1.EdgeRouterRegistry)
], SRoutingHandleView.prototype, "edgeRouterRegistry", void 0);
exports.SRoutingHandleView = SRoutingHandleView = __decorate([
(0, inversify_1.injectable)()
], SRoutingHandleView);
let SLabelView = class SLabelView extends views_1.ShapeView {
render(label, context) {
if (!(0, model_1.isEdgeLayoutable)(label) && !this.isVisible(label, context)) {
return undefined;
}
const vnode = (0, jsx_1.svg)("text", { "class-sprotty-label": true }, label.text);
const subType = (0, model_utils_1.getSubType)(label);
if (subType) {
(0, vnode_utils_1.setAttr)(vnode, 'class', subType);
}
return vnode;
}
};
exports.SLabelView = SLabelView;
exports.SLabelView = SLabelView = __decorate([
(0, inversify_1.injectable)()
], SLabelView);
let SCompartmentView = class SCompartmentView {
render(compartment, context, args) {
const translate = `translate(${compartment.bounds.x}, ${compartment.bounds.y})`;
const vnode = (0, jsx_1.svg)("g", { transform: translate, "class-sprotty-comp": "{true}" }, context.renderChildren(compartment));
const subType = (0, model_utils_1.getSubType)(compartment);
if (subType)
(0, vnode_utils_1.setAttr)(vnode, 'class', subType);
return vnode;
}
};
exports.SCompartmentView = SCompartmentView;
exports.SCompartmentView = SCompartmentView = __decorate([
(0, inversify_1.injectable)()
], SCompartmentView);
let SBezierCreateHandleView = class SBezierCreateHandleView extends SRoutingHandleView {
render(handle, context, args) {
if (args) {
const theRoute = args.route;
if (theRoute && handle.parent instanceof model_2.SRoutableElementImpl) {
const router = this.edgeRouterRegistry.get(handle.parent.routerKind);
const position = router.getHandlePosition(handle.parent, theRoute, handle);
if (position !== undefined) {
const translation = "translate(" + position.x + ", " + position.y + ")";
const textOffsetX = -5.5;
const textOffsetY = 5.5;
const text = (handle.kind === "bezier-add") ? "+" : "-";
const node = (0, jsx_1.svg)("g", { transform: translation, "class-sprotty-routing-handle": true, "class-selected": handle.selected, "class-mouseover": handle.hoverFeedback },
(0, jsx_1.svg)("circle", { r: this.getRadius() }),
(0, jsx_1.svg)("text", { x: textOffsetX, y: textOffsetY, "attrs-text-align": "middle", "style-font-family": "monospace", "style-pointer-events": "none", "style-fill": "white" }, text));
(0, vnode_utils_1.setAttr)(node, 'data-kind', handle.kind);
return node;
}
}
}
// Fallback: Create an empty group
return (0, jsx_1.svg)("g", null);
}
};
exports.SBezierCreateHandleView = SBezierCreateHandleView;
exports.SBezierCreateHandleView = SBezierCreateHandleView = __decorate([
(0, inversify_1.injectable)()
], SBezierCreateHandleView);
let SBezierControlHandleView = class SBezierControlHandleView extends SRoutingHandleView {
render(handle, context, args) {
if (args) {
const theRoute = args.route;
if (theRoute && handle.parent instanceof model_2.SRoutableElementImpl) {
const router = this.edgeRouterRegistry.get(handle.parent.routerKind);
const position = router.getHandlePosition(handle.parent, theRoute, handle);
if (position !== undefined) {
let pathEndPos;
for (let i = 0; i < theRoute.length; i++) {
const elem = theRoute[i];
if (elem.kind === position.kind && elem.pointIndex === position.pointIndex) {
if (handle.kind === 'bezier-control-before') {
pathEndPos = theRoute[i + 1];
}
else {
pathEndPos = theRoute[i - 1];
}
break;
}
}
let node;
if (pathEndPos) {
const coords = `M ${position.x}, ${position.y} L ${pathEndPos.x}, ${pathEndPos.y}`;
node =
(0, jsx_1.svg)("g", { "class-sprotty-routing-handle": true, "class-selected": handle.selected, "class-mouseover": handle.hoverFeedback },
(0, jsx_1.svg)("path", { d: coords, stroke: "grey", "style-stroke-width": "2px" }),
(0, jsx_1.svg)("circle", { cx: position.x, cy: position.y, r: this.getRadius() }));
}
else {
node = (0, jsx_1.svg)("circle", { "class-sprotty-routing-handle": true, "class-selected": handle.selected, "class-mouseover": handle.hoverFeedback, cx: position.x, cy: position.y, r: this.getRadius() });
}
(0, vnode_utils_1.setAttr)(node, 'data-kind', handle.kind);
return node;
}
}
}
// Fallback: Create an empty group
return (0, jsx_1.svg)("g", null);
}
};
exports.SBezierControlHandleView = SBezierControlHandleView;
exports.SBezierControlHandleView = SBezierControlHandleView = __decorate([
(0, inversify_1.injectable)()
], SBezierControlHandleView);
//# sourceMappingURL=views.js.map