UNPKG

sprotty

Version:

A next-gen framework for graphical views

503 lines 24.9 kB
"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