UNPKG

@kieler/klighd-core

Version:

Core KLighD diagram visualization with Sprotty

934 lines 70.9 kB
"use strict"; /* eslint-disable no-continue */ /* * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient * * http://rtsys.informatik.uni-kiel.de/kieler * * Copyright 2022-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-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); }; var ProxyView_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProxyView = void 0; const inversify_1 = require("inversify"); const sprotty_1 = require("sprotty"); const sprotty_protocol_1 = require("sprotty-protocol"); const depth_map_1 = require("../depth-map"); const skgraph_models_1 = require("../skgraph-models"); const views_common_1 = require("../views-common"); const views_styles_1 = require("../views-styles"); const proxy_view_actions_1 = require("./proxy-view-actions"); const proxy_view_cluster_1 = require("./proxy-view-cluster"); const proxy_view_options_1 = require("./proxy-view-options"); const proxy_view_util_1 = require("./proxy-view-util"); /* global document, HTMLElement, MouseEvent */ /** A UIExtension which adds a proxy-view to the Sprotty container. */ let ProxyView = ProxyView_1 = class ProxyView extends sprotty_1.AbstractUIExtension { id() { return ProxyView_1.ID; } containerClass() { return ProxyView_1.ID; } init() { // Send and show proxy-view this.actionDispatcher.dispatch(proxy_view_actions_1.SendProxyViewAction.create(this)); this.actionDispatcher.dispatch(proxy_view_actions_1.ShowProxyViewAction.create()); this.patcher = this.patcherProvider.patcher; // Initialize caches this.filters = new Map(); this.currProxies = []; this.prevModifiedEdges = new Map(); this.renderings = new Map(); this.positions = new Map(); this.distances = new Map(); } initializeContents(containerElement) { // Use temp for initializing currHTMLRoot const temp = document.createElement('div'); this.currHTMLRoot = this.patcher(temp, (0, sprotty_1.html)("div", null)); containerElement.appendChild(temp); } /// ///// Main methods //////// /** * Update step of the proxy-view. Handles everything proxy-view related. * @param model The current SGraph. * @param ctx The rendering context. */ update(model, ctx) { if (!this.proxyViewEnabled) { if (this.prevProxyViewEnabled) { // Prevent excessive patching, only patch if disabled just now this.currHTMLRoot = this.patcher(this.currHTMLRoot, (0, sprotty_1.html)("div", null)); this.prevProxyViewEnabled = this.proxyViewEnabled; } return; } if (!this.currHTMLRoot) { return; } const canvasWidth = model.canvasBounds.width; const canvasHeight = model.canvasBounds.height; const canvas = proxy_view_util_1.Canvas.of(model, ctx.viewport); const root = model.children[0]; // Actually update the document this.currHTMLRoot = this.patcher(this.currHTMLRoot, (0, sprotty_1.html)("svg", { style: { // Set size to whole canvas width: `${canvasWidth}`, height: `${canvasHeight}`, } }, ...this.createAllProxies(root, ctx, canvas))); } /** Returns the proxy rendering for all of currRoot's off-screen children and applies logic, e.g. clustering. */ createAllProxies(root, ctx, canvas) { // Iterate through nodes starting by root, check if node is: // (partially) in bounds -> no proxy, check children // out of bounds -> proxy var _a; // Translate canvas to both Reference Frames const canvasCRF = proxy_view_util_1.Canvas.translateCanvasToCRF(canvas); const canvasGRF = proxy_view_util_1.Canvas.translateCanvasToGRF(canvas); // Calculate size of proxies const size = Math.min(canvasCRF.width, canvasCRF.height) * this.sizePercentage; const fromPercent = 0.01; // The amount of pixels to offset the GRF canvas size by 1%. const onePercentOffsetGRF = Math.min(canvasGRF.width, canvasGRF.height) * fromPercent; /// / Initial nodes //// const depth = (_a = root.properties[ProxyView_1.HIERARCHICAL_OFF_SCREEN_DEPTH]) !== null && _a !== void 0 ? _a : 0; // Reduce canvas size to show proxies early const sizedCanvas = this.showProxiesEarly ? proxy_view_util_1.Canvas.offsetCanvas(canvasGRF, this.showProxiesEarlyNumber * onePercentOffsetGRF) : canvasGRF; const { offScreenNodes, onScreenNodes } = this.getOffAndOnScreenNodes(root, sizedCanvas, depth, ctx); /// / Apply filters //// const filteredOffScreenNodes = this.applyFilters( // The nodes to filter offScreenNodes, // Additional arguments for filters onScreenNodes, canvasCRF, canvasGRF); /// / Clone nodes //// const clonedNodes = this.cloneNodes(filteredOffScreenNodes); /// / Opacity //// const opacityOffScreenNodes = this.calculateOpacity(clonedNodes, canvasGRF); /// / Stacking order //// const orderedOffScreenNodes = this.orderNodes(opacityOffScreenNodes, canvasGRF); /// / Use proxy-rendering as specified by synthesis //// const synthesisRenderedOffScreenNodes = this.getSynthesisProxyRendering(orderedOffScreenNodes, ctx); /// / Calculate transformations //// const transformedOffScreenNodes = synthesisRenderedOffScreenNodes.map(({ node, proxyBounds }) => ({ node, transform: this.getTransform(node, size, proxyBounds, canvasCRF), })); /// / Apply clustering //// const clusteredNodes = this.applyClustering(transformedOffScreenNodes, size, canvasCRF); /// / Route edges to proxies //// const routedEdges = this.routeEdges(clusteredNodes, onScreenNodes, canvasCRF, onePercentOffsetGRF, ctx); /// / Connect off-screen edges //// const segmentConnectors = this.connectEdgeSegments(root, canvasGRF, onePercentOffsetGRF, ctx); /// / Render the proxies //// const proxies = []; this.currProxies = []; // Nodes for (const { node, transform } of clusteredNodes) { // Create a proxy const proxy = this.createProxy(node, transform, canvasGRF, ctx); if (proxy) { proxies.push(proxy); this.currProxies.push({ proxy, transform }); } } // Edges that can be rendered above/below proxies const edgeProxies = []; for (const { edge, transform } of routedEdges.proxyEdges) { // Create an edge proxy const edgeProxy = this.createEdgeProxy(edge, transform, ctx); if (edgeProxy) { edgeProxies.push(edgeProxy); } } if (this.edgesAboveNodes) { // Insert at end to be rendered above nodes proxies.push(...edgeProxies); } else { // Insert at start to be rendered below nodes proxies.unshift(...edgeProxies); } // Edges that should always be rendered below proxies const backEdgeProxies = []; const backEdges = [].concat( // Start with overlays to not have overlays over proxy edges segmentConnectors.overlayEdges, segmentConnectors.proxyEdges, // But routing overlays should still be over segment connectors routedEdges.overlayEdges); for (const { edge, transform } of backEdges) { // Create an edge proxy const edgeProxy = this.createEdgeProxy(edge, transform, ctx); if (edgeProxy) { backEdgeProxies.push(edgeProxy); } } proxies.unshift(...backEdgeProxies); // Clear caches for the next model this.clearPositions(); this.clearDistances(); return proxies; } /** * Returns an object containing lists of all off-screen and on-screen nodes in `currRoot`. * Note that an off-screen node's children aren't included in the list, e.g. only outer-most off-screen nodes are returned. */ getOffAndOnScreenNodes(currRoot, canvasGRF, depth, ctx) { var _a, _b, _c; // For each node check if it's off-screen const offScreenNodes = []; const onScreenNodes = []; for (const node of currRoot.children) { if (node instanceof skgraph_models_1.SKNode) { const bounds = this.getAbsoluteBounds(node); if (this.showProxiesImmediately) { // Show proxies as soon as a node is not completely on-screen if ((bounds.x < canvasGRF.x || bounds.x + bounds.width > canvasGRF.x + canvasGRF.width || bounds.y < canvasGRF.y || bounds.y + bounds.height > canvasGRF.y + canvasGRF.height) && !((canvasGRF.x >= bounds.x && canvasGRF.x + canvasGRF.width <= bounds.x + bounds.width) || (canvasGRF.y >= bounds.y && canvasGRF.y + canvasGRF.height <= bounds.y + bounds.height))) { // Node partially out of bounds and doesn't envelop canvas offScreenNodes.push(node); if (proxy_view_util_1.Canvas.isOnScreen(bounds, canvasGRF)) { // Just partially out of bounds if (node.children.length > 0) { const region = (_a = ctx.depthMap) === null || _a === void 0 ? void 0 : _a.getProvidingRegion(node, ctx.viewport, ctx.renderOptionsRegistry); if (!(this.useDetailLevel && (region === null || region === void 0 ? void 0 : region.detail)) || (0, depth_map_1.isDetailWithChildren)(region.detail)) { // Has children, recursively check them const childRes = this.getOffAndOnScreenNodes(node, canvasGRF, depth, ctx); offScreenNodes.push(...childRes.offScreenNodes); onScreenNodes.push(...childRes.onScreenNodes); } } } else if (depth !== 0 && node.children.length > 0) { // Fully out of bounds /** depth > 0 or < 0 (see {@link HIERARCHICAL_OFF_SCREEN_DEPTH}), go further in */ const childRes = this.getOffAndOnScreenNodes(node, canvasGRF, depth - 1, ctx); offScreenNodes.push(...childRes.offScreenNodes); onScreenNodes.push(...childRes.onScreenNodes); } } else { // Node completely in bounds or envelops canvas onScreenNodes.push(node); if (node.children.length > 0) { const region = (_b = ctx.depthMap) === null || _b === void 0 ? void 0 : _b.getProvidingRegion(node, ctx.viewport, ctx.renderOptionsRegistry); if (!(this.useDetailLevel && (region === null || region === void 0 ? void 0 : region.detail)) || (0, depth_map_1.isDetailWithChildren)(region.detail)) { // Has children, recursively check them const childRes = this.getOffAndOnScreenNodes(node, canvasGRF, depth, ctx); offScreenNodes.push(...childRes.offScreenNodes); onScreenNodes.push(...childRes.onScreenNodes); } } } } else if (!proxy_view_util_1.Canvas.isOnScreen(bounds, canvasGRF)) { // Normal proxy-view behaviour // Node out of bounds offScreenNodes.push(node); if (depth !== 0 && node.children.length > 0) { /** depth > 0 or < 0 (see {@link HIERARCHICAL_OFF_SCREEN_DEPTH}), go further in */ const childRes = this.getOffAndOnScreenNodes(node, canvasGRF, depth - 1, ctx); offScreenNodes.push(...childRes.offScreenNodes); onScreenNodes.push(...childRes.onScreenNodes); } } else { // Node in bounds onScreenNodes.push(node); if (node.children.length > 0) { const region = (_c = ctx.depthMap) === null || _c === void 0 ? void 0 : _c.getProvidingRegion(node, ctx.viewport, ctx.renderOptionsRegistry); if (!(this.useDetailLevel && (region === null || region === void 0 ? void 0 : region.detail)) || (0, depth_map_1.isDetailWithChildren)(region.detail)) { // Has children, recursively check them const childRes = this.getOffAndOnScreenNodes(node, canvasGRF, depth, ctx); offScreenNodes.push(...childRes.offScreenNodes); onScreenNodes.push(...childRes.onScreenNodes); } } } } } return { offScreenNodes, onScreenNodes }; } /** * Returns all `offScreenNodes` matching the enabled filters. * @param offScreenNodes The nodes to filter. * @param onScreenNodes Argument for filters. * @param canvasGRF Argument for filters. */ applyFilters(offScreenNodes, onScreenNodes, canvasCRF, canvasGRF) { return offScreenNodes.filter((node) => this.canRenderNode(node) && node.opacity > 0 && Array.from(this.filters.values()).every((filter) => filter({ node, offScreenNodes, onScreenNodes, canvasCRF, canvasGRF, distance: this.getNodeDistanceToCanvas(node, canvasGRF), }))); } /** Performs a shallow copy of the nodes so that the original nodes aren't mutated. */ cloneNodes(offScreenNodes) { return offScreenNodes.map((node) => Object.create(node)); } /** Calculates the opacities of `offScreenNodes`. */ calculateOpacity(offScreenNodes, canvasGRF) { const res = offScreenNodes; if (this.opacityByDistanceEnabled) { for (const node of res) { // Reduce opacity such that the node is fully transparent when the node's distance is >= DISTANCE_DISTANT const opacityReduction = this.getNodeDistanceToCanvas(node, canvasGRF) / ProxyView_1.DISTANCE_DISTANT; const minOpacity = 0.1; node.opacity = Math.max(minOpacity, node.opacity - opacityReduction); } } if (this.opacityBySelected && proxy_view_util_1.SelectedElementsUtil.areNodesSelected()) { // Only change opacity if there are selected nodes for (const node of res) { if ((0, proxy_view_util_1.isSelectedOrConnectedToSelected)(node)) { // If selected itself or connected to a selected node, this node should be opaque node.opacity = 1; } else { // Node not relevant to current selection context, decrease opacity // If opaque, the node should be 50% transparent const opacityReduction = 0.5; node.opacity = Math.max(0, node.opacity * opacityReduction); } } } return res; } /** * Orders `offScreenNodes` such that the contextually most relevant * nodes appear at the end - therefore being rendered on top. */ orderNodes(offScreenNodes, canvasGRF) { const res = [...offScreenNodes]; if (!this.clusteringEnabled) { // Makes no sense to order when clustering is enabled since proxies cannot be stacked /* Order these stacking order criteria such that each criterion is more important the previous one, i.e. the least important criterion is at the start and the most important one is at the end */ if (this.stackingOrderByDistance) { // Distant nodes at start, close nodes at end res.sort((n1, n2) => this.getNodeDistanceToCanvas(n2, canvasGRF) - this.getNodeDistanceToCanvas(n1, canvasGRF)); } if (this.stackingOrderByOpacity) { // Most transparent nodes at start, least transparent ones at end res.sort((n1, n2) => n1.opacity - n2.opacity); } if (this.stackingOrderBySelected) { // Move selected nodes to end (and keep previous ordering, e.g. "grouping" by selected) res.sort((n1, n2) => (n1.selected === n2.selected ? 0 : n1.selected ? 1 : -1)); } } return res; } /** Returns the nodes updated to use the rendering specified by the synthesis. */ getSynthesisProxyRendering(offScreenNodes, ctx) { const res = []; for (const node of offScreenNodes) { // Fallback, if property undefined use universal proxy rendering for this node let proxyBounds = node.bounds; if (this.useSynthesisProxyRendering && node.properties && node.properties[ProxyView_1.PROXY_RENDERING_PROPERTY]) { const data = node.properties[ProxyView_1.PROXY_RENDERING_PROPERTY]; const kRendering = (0, views_common_1.getKRendering)(data, ctx); if (kRendering && kRendering.properties['klighd.lsp.calculated.bounds']) { // Proxy rendering available, update data node.data = data; // Also update the bounds proxyBounds = kRendering.properties['klighd.lsp.calculated.bounds']; } } res.push({ node, proxyBounds }); } return res; } /** Applies clustering to all `offScreenNodes` until there's no more overlap. Cluster-proxies are returned as VNodes. */ applyClustering(offScreenNodes, size, canvasCRF) { var _a, _b; if (!this.clusteringEnabled) { return offScreenNodes; } // List containing groups of indices of overlapping proxies // Could use a set of sets here, not needed since the same group cannot appear twice let overlapIndexGroups = [[]]; let res = offScreenNodes; // Make sure each cluster id is unique let clusterIDOffset = 0; while (overlapIndexGroups.length > 0) { overlapIndexGroups = []; if (this.clusteringSweepLine) { // Sort res primarily by leftmost x value, secondarily by uppermost y value, i.e. // res[0] has leftmost proxy (and of all leftmost proxies it's the uppermost one) res = res.sort(({ transform: t1 }, { transform: t2 }) => { let result = t1.x - t2.x; if (result === 0) { result = t1.y - t2.y; } return result; }); for (let i = 0; i < res.length; i++) { if (!this.clusteringCascading && (0, proxy_view_util_1.anyContains)(overlapIndexGroups, i)) { // i already in an overlapIndexGroup, prevent redundant clustering continue; } // New list for current overlap group const currOverlapIndexGroup = []; // Check proxies to the left of the current one's right border for overlap const transform1 = res[i].transform; const right = transform1.x + transform1.width; const bottom = transform1.y + transform1.height; for (let j = 0; j < res.length; j++) { if (i === j || (0, proxy_view_util_1.anyContains)(overlapIndexGroups, j)) { // Every proxy overlaps with itself or // j already in an overlapIndexGroup, prevent redundant clustering continue; } const transform2 = res[j].transform; if (transform2.x > right) { // Too far right, no need to check break; } else if (transform2.x === right && transform2.y > bottom) { // Too far down, no need to check break; } else if ((0, proxy_view_util_1.checkOverlap)(transform1, transform2)) { // Proxies at i and j overlap currOverlapIndexGroup.push(j); } } if (currOverlapIndexGroup.length > 0) { // This proxy overlaps currOverlapIndexGroup.push(i); overlapIndexGroups.push(currOverlapIndexGroup); } } } else { for (let i = 0; i < res.length; i++) { if (!this.clusteringCascading && (0, proxy_view_util_1.anyContains)(overlapIndexGroups, i)) { // i already in an overlapIndexGroup, prevent redundant clustering continue; } // New list for current overlap group const currOverlapIndexGroup = []; // Check next proxies for overlap for (let j = i + 1; j < res.length; j++) { if ((0, proxy_view_util_1.checkOverlap)(res[i].transform, res[j].transform)) { // Proxies at i and j overlap currOverlapIndexGroup.push(j); } } if (currOverlapIndexGroup.length > 0) { // This proxy overlaps currOverlapIndexGroup.push(i); overlapIndexGroups.push(currOverlapIndexGroup); } } } if (overlapIndexGroups.length <= 0) { // No more overlap, clustering is done break; } if (this.clusteringCascading) { // Join groups containing at least 1 same index overlapIndexGroups = (0, proxy_view_util_1.joinTransitiveGroups)(overlapIndexGroups); } // Add cluster proxies for (let i = 0; i < overlapIndexGroups.length; i++) { // Add a cluster for each group const group = overlapIndexGroups[i]; // Get all nodes of the current group const currGroupNodes = res.filter((_, index) => group.includes(index)); // Calculate position to put cluster proxy at, e.g. average of this group's positions let numProxiesInCluster = 0; let x = 0; let y = 0; let opacity = 1; for (const { node, transform } of currGroupNodes) { // Weigh coordinates by the number of proxies in the current proxy (which might be a cluster) const numProxiesInCurr = (_a = transform.numProxies) !== null && _a !== void 0 ? _a : 1; numProxiesInCluster += numProxiesInCurr; x += transform.x * numProxiesInCurr; y += transform.y * numProxiesInCurr; if (this.clusterTransparent) { opacity += ((_b = node.opacity) !== null && _b !== void 0 ? _b : 1) * numProxiesInCurr; } } x /= numProxiesInCluster; y /= numProxiesInCluster; if (this.clusterTransparent) { // +1 since it starts at 1 opacity /= numProxiesInCluster + 1; } // Cap opacity in [0,1] opacity = (0, proxy_view_util_1.capNumber)(opacity, 0, 1); ({ x, y } = proxy_view_util_1.Canvas.capToCanvas({ x, y, width: size, height: size }, canvasCRF)); // Also make sure the calculated positions are still capped to the border (no floating proxies) let floating = false; if (y > canvasCRF.y && y < canvasCRF.y + canvasCRF.height - size && (x > canvasCRF.x || x < canvasCRF.x + canvasCRF.width - size)) { x = x > (canvasCRF.width - size) / 2 ? canvasCRF.x + canvasCRF.width - size : canvasCRF.x; floating = true; } else if (x > canvasCRF.x && x < canvasCRF.x + canvasCRF.width - size && (y > canvasCRF.y || y < canvasCRF.y + canvasCRF.height - size)) { y = y > (canvasCRF.height - size) / 2 ? canvasCRF.y + canvasCRF.height - size : canvasCRF.y; floating = true; } if (floating) { // Readjust if it was previously floating ; ({ x, y } = proxy_view_util_1.Canvas.capToCanvas({ x, y, width: size, height: size }, canvasCRF)); } const clusterNode = (0, proxy_view_cluster_1.getClusterRendering)(`cluster-${clusterIDOffset + i}-proxy`, numProxiesInCluster, size, x, y, opacity); res.push({ node: clusterNode || { opacity }, transform: { x, y, scale: 1, width: size, height: size, // Store the number of proxies in this cluster in case the cluster is clustered later on numProxies: numProxiesInCluster, }, }); } // Filter all overlapping nodes // eslint-disable-next-line no-loop-func res = res.filter((_, index) => !(0, proxy_view_util_1.anyContains)(overlapIndexGroups, index)); clusterIDOffset += overlapIndexGroups.length; } return res; } /** Routes edges from `onScreenNodes` to the corresponding proxies of `nodes`. */ routeEdges(nodes, onScreenNodes, canvasCRF, onePercentOffsetGRF, ctx) { if (!(this.straightEdgeRoutingEnabled || this.alongBorderRoutingEnabled)) { // Don't create edge proxies return { proxyEdges: [], overlayEdges: [] }; } // Reset opacity before changing it this.resetEdgeOpacity(this.prevModifiedEdges); const modifiedEdges = new Map(); const proxyEdges = []; const overlayEdges = []; for (const { node, transform } of nodes) { if (node instanceof skgraph_models_1.SKNode) { // Incoming edges for (const edge of node.incomingEdges) { if (edge.routingPoints.length > 1 && onScreenNodes.some((node2) => node2.id === edge.sourceId)) { // Only reroute actual edges with end at on-screen node // Proxy is target, node is source const proxyConnector = edge.routingPoints[edge.routingPoints.length - 1]; const nodeConnector = edge.routingPoints[0]; const proxyEdge = this.rerouteEdge(node, transform, edge, modifiedEdges, nodeConnector, proxyConnector, false, canvasCRF, onePercentOffsetGRF, ctx); if (proxyEdge) { // Can't use transform for proxyEdge since it's already translated proxyEdges.push(proxyEdge); // Overlay original edge overlayEdges.push(this.getOverlayEdge(edge, canvasCRF, ctx)); } } } // Outgoing edges for (const edge of node.outgoingEdges) { if (edge.routingPoints.length > 1 && onScreenNodes.some((node2) => node2.id === edge.targetId)) { // Only reroute actual edges with start at on-screen node // Proxy is source, node is target const proxyConnector = edge.routingPoints[0]; const nodeConnector = edge.routingPoints[edge.routingPoints.length - 1]; const proxyEdge = this.rerouteEdge(node, transform, edge, modifiedEdges, nodeConnector, proxyConnector, true, canvasCRF, onePercentOffsetGRF, ctx); if (proxyEdge) { // Can't use transform for proxyEdge since it's already translated proxyEdges.push(proxyEdge); // Overlay original edge overlayEdges.push(this.getOverlayEdge(edge, canvasCRF, ctx)); } } } } } // New modified edges this.prevModifiedEdges = modifiedEdges; return { proxyEdges, overlayEdges }; } /** * Returns an edge rerouted to the proxy. * `nodeConnector` and `proxyConnector` are the endpoints of the original edge. * @param `outgoing` Whether the edge is outgoing from the proxy. */ rerouteEdge(node, transform, edge, modifiedEdges, nodeConnector, proxyConnector, outgoing, canvasCRF, onePercentOffsetGRF, ctx) { // TODO: on spline renderings, always bundle the bend points together in pairs/3s, such that the edge remains smooth. // Connected to node, just calculate absolute coordinates + basic translation const parentPos = this.getAbsolutePosition(node.parent); const parentTranslated = proxy_view_util_1.Canvas.translateToCRF(this.getAbsolutePosition(edge.parent), canvasCRF); if (!this.edgesToOffScreenPoint && !sprotty_protocol_1.Bounds.includes(canvasCRF, nodeConnector)) { // Would be connected to an off-screen point, don't show the edge return undefined; } // Connected to proxy, use ratio to calculate where to connect to the proxy const proxyPointRelative = node.parentToLocal(proxyConnector); const proxyRatioX = proxyPointRelative.x / node.bounds.width; const proxyRatioY = proxyPointRelative.y / node.bounds.height; const proxyTranslatedRelative = { x: transform.x + transform.width * proxyRatioX, y: transform.y + transform.height * proxyRatioY, }; // The GRF point where the proxy edge is connected to the proxy. const proxyConnectorGRF = sprotty_protocol_1.Point.subtract(proxy_view_util_1.Canvas.translateToGRF(proxyTranslatedRelative, canvasCRF), parentPos); // The GRF bounds of the proxy. let proxyGRFRelToParent = proxy_view_util_1.Canvas.translateToGRF(transform, canvasCRF); proxyGRFRelToParent = { x: proxyGRFRelToParent.x - parentPos.x, y: proxyGRFRelToParent.y - parentPos.y, width: proxyGRFRelToParent.width, height: proxyGRFRelToParent.height, }; // Keep direction of edge const source = outgoing ? proxyConnectorGRF : nodeConnector; const target = outgoing ? nodeConnector : proxyConnectorGRF; // Calculate all routing points const routingPoints = []; if (this.straightEdgeRoutingEnabled) { // Straight edge from source to target routingPoints.push(source, target); } else if (this.alongBorderRoutingEnabled) { // Potentially need more points than just source and target const canvasGRFRelToParent = proxy_view_util_1.Canvas.offsetCanvas(proxy_view_util_1.Canvas.translateCanvasToGRF(canvasCRF), { left: -parentPos.x, right: parentPos.x, top: -parentPos.y, bottom: parentPos.y, }); const leftOffset = onePercentOffsetGRF; const rightOffset = onePercentOffsetGRF; const topOffset = onePercentOffsetGRF; const bottomOffset = onePercentOffsetGRF; const offsetRect = { left: leftOffset, right: rightOffset, top: topOffset, bottom: bottomOffset }; // Canvas dimensions with offset, so as to keep the edge on the canvas const canvasOffLeft = canvasGRFRelToParent.x + leftOffset; const canvasOffRight = canvasGRFRelToParent.x + canvasGRFRelToParent.width - rightOffset; const canvasOffTop = canvasGRFRelToParent.y + topOffset; const canvasOffBottom = canvasGRFRelToParent.y + canvasGRFRelToParent.height - bottomOffset; const canvasOffset = proxy_view_util_1.Canvas.offsetCanvas(canvasGRFRelToParent, offsetRect); // Appends the point to routingPoints const add = (p) => routingPoints.push(p); // Caps the point to the canvas const cap = (p) => // TODO: a GRF-compatible cap function would be nice here. proxy_view_util_1.Canvas.translateToGRF(proxy_view_util_1.Canvas.capToCanvas(proxy_view_util_1.Canvas.translateToCRF(p, canvasCRF), proxy_view_util_1.Canvas.translateCanvasToCRF(canvasOffset)), canvasCRF); // Composition add o cap const addCap = (p) => add(cap(p)); // Returns true if the point is inside the proxy // Add a little padding (just one pixel) to the proxy bounds, so that rounding errors do not cause additional/flickering routing points. const proxyEdgePadding = 1; const proxyGRFRelToParentWithPadding = { x: proxyGRFRelToParent.x - proxyEdgePadding, y: proxyGRFRelToParent.y - proxyEdgePadding, width: proxyGRFRelToParent.width + 2 * proxyEdgePadding, height: proxyGRFRelToParent.height + 2 * proxyEdgePadding, }; const outsideProxy = (p) => !sprotty_protocol_1.Bounds.includes(proxyGRFRelToParentWithPadding, p); if (this.simpleAlongBorderRouting) { // Just cap each routing point to the canvas, can cause strange artifacts if an edge e.g. oscillates edge.routingPoints // Cap to canvas .map(cap) // Don't add points that are inside the proxy .filter(outsideProxy) // Cap to canvas and add to routingPoints .forEach(add); } else { /// / Calculate point where edge leaves canvas let prevPoint = source; let canvasEdgeIntersection; for (let i = 0; i < edge.routingPoints.length; i++) { // Check if p is off-screen to find intersection between (prevPoint to p) and canvas // Traverse routingPoints from the end for outgoing edges to match with prevPoint const p = edge.routingPoints[i]; const intersection = (0, proxy_view_util_1.getIntersection)(prevPoint, p, canvasGRFRelToParent); if (intersection) { // Found an intersection canvasEdgeIntersection = intersection; if (outsideProxy(canvasEdgeIntersection)) { // Don't add a point inside of the proxy addCap(canvasEdgeIntersection); } } // Add p to keep routing points consistent prevPoint = p; if (sprotty_protocol_1.Bounds.includes(canvasGRFRelToParent, p) && outsideProxy(p)) { // Don't add a point that is off-screen or inside the proxy add(p); } } if (!canvasEdgeIntersection) { // Should never be the case since one node has to be off-screen for a proxy to be created // Therefore the edge must intersect with the canvas return undefined; } /// / Calculate points on path to proxy near canvas const preferLeft = proxyConnectorGRF.x < (canvasOffLeft + canvasOffRight) / 2; const preferTop = proxyConnectorGRF.y < (canvasOffTop + canvasOffBottom) / 2; const borderPoints = proxy_view_util_1.Canvas.routeAlongBorder(canvasEdgeIntersection, canvasOffset, transform, canvasGRFRelToParent, preferLeft, preferTop); // Remove points inside of proxy and add remaining borderPoints.filter(outsideProxy).forEach(addCap); } /// / Finally, add source at its correct spot // Avoid duplicate source/target points. if (routingPoints[0] !== source) { routingPoints.unshift(source); } if (routingPoints[routingPoints.length - 1] !== target) { routingPoints.push(target); } } else { // Should never be the case, must be called with a routing strategy enabled return undefined; } // Clone the edge so as to not change the real one const clone = Object.create(edge); // Set attributes clone.routingPoints = routingPoints; clone.junctionPoints = []; clone.id += '-rerouted'; clone.data = this.placeDecorator(edge.data, ctx, routingPoints[routingPoints.length - 2], routingPoints[routingPoints.length - 1]); clone.opacity = node.opacity; if (this.transparentEdges) { // Fade out the original edge and store its previous opacity const id = (0, proxy_view_util_1.getProxyId)(edge.id); modifiedEdges.set(id, [edge, edge.opacity]); edge.opacity = 0; } return { edge: clone, transform: Object.assign(Object.assign({}, parentTranslated), { scale: canvasCRF.zoom }) }; } /** Returns an edge that can be overlayed over the given `edge` to simulate a fade-out effect. */ getOverlayEdge(edge, canvas, ctx) { // Color/opacity for fade out effect const color = { red: 255, green: 255, blue: 255 }; const opacity = 0.8; const parentTranslated = proxy_view_util_1.Canvas.translateToCRF(this.getAbsolutePosition(edge.parent), canvas); const overlay = Object.create(edge); overlay.id += '-overlay'; overlay.opacity = opacity; overlay.data = this.changeColor(overlay.data, ctx, color); return { edge: overlay, transform: Object.assign(Object.assign({}, parentTranslated), { scale: canvas.zoom }) }; } /** Connects off-screen edges. */ connectEdgeSegments(root, canvasGRF, onePercentOffsetGRF, ctx) { if (!this.segmentProxiesEnabled) { return { proxyEdges: [], overlayEdges: [] }; } const offsetRect = { left: onePercentOffsetGRF, right: onePercentOffsetGRF, top: onePercentOffsetGRF, bottom: onePercentOffsetGRF, }; const canvasOffset = proxy_view_util_1.Canvas.offsetCanvas(canvasGRF, offsetRect); const proxyEdges = []; const overlayEdges = []; // Get all edges that are partially off-screen const partiallyOffScreenEdges = this.getPartiallyOffScreenEdges(root, canvasOffset); // Connect intersections with canvas for (const edge of partiallyOffScreenEdges) { const parentPos = this.getAbsolutePosition(edge.parent); // Find all intersections let prevPoint = sprotty_protocol_1.Point.add(parentPos, edge.routingPoints[0]); const canvasEdgeIntersections = []; for (let i = 1; i < edge.routingPoints.length; i++) { const p = sprotty_protocol_1.Point.add(parentPos, edge.routingPoints[i]); const intersection = (0, proxy_view_util_1.getIntersection)(prevPoint, p, canvasOffset); if (intersection) { // Found an intersection canvasEdgeIntersections.push({ intersection, fromOnScreen: sprotty_protocol_1.Bounds.includes(canvasOffset, prevPoint), index: i - 1, }); } prevPoint = p; } if (canvasEdgeIntersections.length < 2) { // Not enoug intersections to form a connection continue; } // Make sure to only connect on-screen to off-screen to on-screen (on-off-on) intersections, not off-on-off const routingPointIndices = []; let { intersection: prevIntersection, fromOnScreen: prevFromOnScreen, index: prevIndex, } = canvasEdgeIntersections[0]; for (let i = 1; i < canvasEdgeIntersections.length; i++) { const { intersection, fromOnScreen, index } = canvasEdgeIntersections[i]; if (prevFromOnScreen) { // Can safely be connected, therefore store routing point indices and path along border between intersections const ps = proxy_view_util_1.Canvas.routeAlongBorder(prevIntersection, canvasOffset, intersection, canvasOffset); ps.unshift(prevIntersection); ps.push(intersection); routingPointIndices.push({ to: prevIndex, from: index, ps }); } prevIntersection = intersection; prevFromOnScreen = fromOnScreen; prevIndex = index; } // Finally, reconstruct the original path with the connecting points if (routingPointIndices.length > 0) { const segmentConnector = Object.create(edge); segmentConnector.id += '-segmentConnector'; const routingPoints = []; let prevFrom = 0; for (const { to, from, ps } of routingPointIndices) { // Add points from previous intersection up to current one routingPoints.push(...segmentConnector.routingPoints.slice(prevFrom, to + 1)); // Add intersection path, e.g. the segment connector routingPoints.push(...ps.map((p) => sprotty_protocol_1.Point.subtract(p, parentPos))); prevFrom = from + 1; } // Add last couple points routingPoints.push(...segmentConnector.routingPoints.slice(prevFrom, segmentConnector.routingPoints.length)); segmentConnector.routingPoints = routingPoints; proxyEdges.push({ edge: segmentConnector, transform: Object.assign(Object.assign({}, proxy_view_util_1.Canvas.translateToCRF(parentPos, canvasGRF)), { scale: canvasGRF.zoom }), }); // Remember to fade out original edge overlayEdges.push(this.getOverlayEdge(edge, canvasGRF, ctx)); } } return { proxyEdges, overlayEdges }; } /** Returns all edges that are both on- & off-screen. */ getPartiallyOffScreenEdges(currRoot, canvas) { // For each edge check if it's partially off-screen const partiallyOffScreenEdges = []; const pos = this.getAbsolutePosition(currRoot); // Can just use this one loop since both edges and nodes are present in children for (const child of currRoot.children) { if (child instanceof skgraph_models_1.SKEdge) { // Check if it's on- & off-screen let offScreen = false; let onScreen = false; for (let p of child.routingPoints) { p = sprotty_protocol_1.Point.add(pos, p); if (sprotty_protocol_1.Bounds.includes(canvas, p)) { onScreen = true; } else { offScreen = true; } if (onScreen && offScreen) { partiallyOffScreenEdges.push(child); break; } } } else if (child instanceof skgraph_models_1.SKNode) { // Recursively check its children partiallyOffScreenEdges.push(...this.getPartiallyOffScreenEdges(child, canvas)); } } return partiallyOffScreenEdges; } /** Returns the proxy rendering for an off-screen node. */ createProxy(node, transform, canvasGRF, ctx) { var _a; if (!(node instanceof skgraph_models_1.SKNode)) { // VNode, this is a predefined rendering (e.g. cluster) (0, proxy_view_util_1.updateTransform)(node, transform, this.viewerOptions.baseDiv); return node; } if (node.opacity <= 0) { // Don't render invisible nodes return undefined; } // Check if this node's proxy should be highlighted const highlight = node.selected || (this.highlightSelected && (0, proxy_view_util_1.isSelectedOrConnectedToSelected)(node)); const { opacity } = node; // Get VNode const id = (0, proxy_view_util_1.getProxyId)(node.id); let vnode = this.renderings.get(id); if (!vnode || vnode.selected !== highlight) { // Node hasn't been rendered yet (cache empty for this node) or the attributes don't match // Change its id to differ from the original node node.id = id; // Clear children, proxies don't show nested nodes (but keep labels) node.children = node.children.filter((theNode) => theNode instanceof skgraph_models_1.SKLabel); const scale = (_a = transform.scale) !== null && _a !== void 0 ? _a : 1; // Add the proxy's scale to the data node.data = this.getNodeData(node.data, scale); // Proxies should never appear to be selected (even if their on-screen counterpart is selected) // unless highlighting is enabled node.selected = highlight; // Render this node as opaque to change opacity later on node.opacity = 1; vnode = ctx.forceRenderElement(node); if (vnode) { // New rendering, set ProxyVNode attributes vnode.selected = highlight; vnode.proxy = true; // Add usual mouse interaction this.addMouseInteraction(vnode, node); } } if (vnode) { // Store this node this.renderings.set(id, vnode); // Place proxy at the calculated position (0, proxy_view_util_1.updateTransform)(vnode, transform, this.viewerOptions.baseDiv); // Update its opacity (0, proxy_view_util_1.updateOpacity)(vnode, opacity, this.viewerOptions.baseDiv); // Update whether it should be click-through (0, proxy_view_util_1.updateClickThrough)(vnode, !this.interactiveProxiesEnabled || this.clickThrough, this.viewerOptions.baseDiv); } return vnode; } /** Let the mouseTool decorate this proxy rendering to activate all KLighD- and Proxy-specific mouse interactions. */ addMouseInteraction(vnode, element) { if ((0, sprotty_1.isThunk)(vnode)) { return vnode; } return this.mouseTool.decorate(vnode, element); } /** Returns the proxy rendering for an edge. */ createEdgeProxy(edge, transform, ctx) { if (edge.opacity <= 0) { // Don't draw an invisible edge return undefined; } // Change it