@kieler/klighd-core
Version:
Core KLighD diagram visualization with Sprotty
934 lines • 70.9 kB
JavaScript
"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