@kieler/klighd-core
Version:
Core KLighD diagram visualization with Sprotty
927 lines • 39.8 kB
JavaScript
;
/*
* 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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectedElementsUtilActionHandler = exports.SelectedElementsUtil = exports.isSelectedOrConnectedToSelected = exports.isConnectedToAny = exports.isOutgoingToAny = exports.isIncomingToAny = exports.joinTransitiveGroups = exports.anyContains = exports.updateClickThrough = exports.updateOpacity = exports.updateTransform = exports.getIntersection = exports.distanceBetweenBounds = exports.capNumber = exports.asBounds = exports.checkOverlap = exports.isInBounds = exports.getNodeId = exports.getProxyId = exports.isProxyRendering = exports.isProxy = exports.Rect = exports.Canvas = exports.PROXY_SUFFIX = void 0;
const inversify_1 = require("inversify");
const sprotty_1 = require("sprotty");
const sprotty_protocol_1 = require("sprotty-protocol");
const actions_1 = require("../actions/actions");
const skgraph_models_1 = require("../skgraph-models");
/* global document, Element, SVGElement */
/** Suffix of a proxy's ID. */
exports.PROXY_SUFFIX = '$proxy';
var Canvas;
(function (Canvas) {
/// / Get Canvas ////
/**
* Creates a canvas in CRF.
* @param boundsOrRoot The canvas' bounds or the root element.
* @param viewport The viewport.
* @returns The canvas.
*/
function of(boundsOrRoot, viewport) {
const canvasBounds = (0, sprotty_protocol_1.isBounds)(boundsOrRoot) ? boundsOrRoot : boundsOrRoot.canvasBounds;
return Object.assign(Object.assign({}, canvasBounds), { scroll: viewport.scroll, zoom: viewport.zoom });
}
Canvas.of = of;
/// / Translation ////
/**
* Returns the bounds translated from the GRF to the CRF.
* E.g. calculates its position & width/height according to scroll and zoom.
* Inverse to {@link translateToGRF()}.
* @param originalBounds The bounds/point/dimension in the GRF.
* @param canvas The canvas.
* @returns The bounds translated to the CRF.
*/
function translateToCRF(originalBounds, canvas) {
const bounds = asBounds(originalBounds);
const { scroll } = canvas;
const { zoom } = canvas;
return {
x: (bounds.x - scroll.x) * zoom,
y: (bounds.y - scroll.y) * zoom,
width: bounds.width * zoom,
height: bounds.height * zoom,
};
}
Canvas.translateToCRF = translateToCRF;
/**
* Returns the bounds translated from the CRF to the GRF.
* Inverse to {@link translateToCRF()}.
* @param originalBounds The bounds/point/dimension in the CRF.
* @param canvas The canvas.
* @returns The bounds translated to the GRF.
*/
function translateToGRF(originalBounds, canvas) {
const bounds = asBounds(originalBounds);
const { scroll } = canvas;
const { zoom } = canvas;
return {
x: bounds.x / zoom + scroll.x,
y: bounds.y / zoom + scroll.y,
width: bounds.width / zoom,
height: bounds.height / zoom,
};
}
Canvas.translateToGRF = translateToGRF;
/**
* Adds `p1` and `p2` and translates the result to the CRF.
* @param p1 The first point.
* @param p2 The second point.
* @param canvas The canvas.
*/
function translateToCRFAdd(p1, p2, canvas) {
return translateToCRF(sprotty_protocol_1.Point.add(p1, p2), canvas);
}
Canvas.translateToCRFAdd = translateToCRFAdd;
/**
* Translates the canvas from the GRF to the CRF, if not already in CRF.
* Inverse to {@link translateCanvasToGRF()}.
* @param canvas The canvas.
* @returns The canvas translated to the CRF.
*/
function translateCanvasToCRF(canvas) {
if (canvas.isInGRF) {
return Object.assign(Object.assign(Object.assign({}, canvas), translateToCRF(canvas, canvas)), { isInGRF: false });
}
return canvas;
}
Canvas.translateCanvasToCRF = translateCanvasToCRF;
/**
* Translates the canvas from the CRF to the GRF, if not already in GRF.
* Inverse to {@link translateCanvasToCRF()}.
* @param canvas The canvas.
* @returns The canvas translated to the GRF.
*/
function translateCanvasToGRF(canvas) {
if (canvas.isInGRF) {
return canvas;
}
return Object.assign(Object.assign(Object.assign({}, canvas), translateToGRF(canvas, canvas)), { isInGRF: true });
}
Canvas.translateCanvasToGRF = translateCanvasToGRF;
/// / Functions invariant to Reference Frame ////
/**
* Checks if `bounds` is (partially) on-screen.
* Note that `bounds` and `canvas` need to be in the same Reference Frame.
* @param bounds The bounds to check.
* @returns `true` if `b` is (partially) on-screen.
*/
function isOnScreen(bounds, canvas) {
return isInBounds(bounds, canvas);
}
Canvas.isOnScreen = isOnScreen;
/**
* Returns the distance between the bounds and the canvas.
* @see {@link distanceBetweenBounds()} for an explanation on how the distance is calculated.
* Note that `bounds` and `canvas` need to be in the same Reference Frame.
*
* @param bounds The bounds/point to calculate the distance to the canvas for.
* @param canvas The canvas.
* @returns The distance between the bounds and the canvas.
*/
function distance(bounds, canvas) {
const dist = distanceBetweenBounds(bounds, canvas);
return dist * (canvas.isInGRF ? canvas.zoom : 1);
}
Canvas.distance = distance;
/**
* Performs along-border-routing from `from` to `to`, both of which need to be at their respective border already.
* Note that `from` and `to` are not part of the returned path.
* @param from The bounds/point to route along the border from.
* @param fromBorder The border for `from`.
* @param to The bounds/point to route along the border to.
* @param toBorder The border for `to`.
* @param preferLeft Whether routing left should be preferred when `from` and `to` are vertically opposite of each other.
* @param preferTop Whether routing top should be preferred when `from` and `to` are horizontally opposite of each other.
* @returns A path along the border from `from` to `to`. Exclusive, e.g. (from, to).
*/
function routeAlongBorder(from, fromBorder, to, toBorder, preferLeft = true, preferTop = false) {
const res = [];
const toRect = Rect.fromBounds(asBounds(to));
const toBorderRect = Rect.fromBounds(toBorder);
const fromRect = Rect.fromBounds(asBounds(from));
const fromBorderRect = Rect.fromBounds(fromBorder);
let x;
let y;
if (fromRect.left === fromBorderRect.left) {
// from at the left
x = fromBorderRect.left;
if (toRect.left === toBorderRect.left) {
// to at the left
// Nothing to do
}
else if (toRect.right === toBorderRect.right) {
// to at the right
if (toRect.top === toBorderRect.top) {
// to at the top, add a point to top left
y = fromBorderRect.top;
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom, add a point to bottom left
y = fromBorderRect.bottom;
}
else {
// to in between top and bottom
// Need 2 routing points, choose the preferred one
y = preferTop ? fromBorderRect.top : fromBorderRect.bottom;
res.push({ x, y });
// 2nd routing point
x = fromBorderRect.right;
}
}
else {
// to in between left and right
// eslint-disable-next-line no-lonely-if
if (toRect.top === toBorderRect.top) {
// to at the top, add a point to top left
y = fromBorderRect.top;
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom, add a point to bottom left
y = fromBorderRect.bottom;
}
else {
// Should never be the case, would be hovering somewhere
throw new Error('Invalid case in routeAlongBorder reached.');
}
}
}
else if (fromRect.right === fromBorderRect.right) {
// from at the right
x = fromBorderRect.right;
if (toRect.left === toBorderRect.left) {
// to at the left
if (toRect.top === toBorderRect.top) {
// to at the top, add a point to top right
y = fromBorderRect.top;
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom, add a point to bottom right
y = fromBorderRect.bottom;
}
else {
// to in between top and bottom
// Need 2 routing points, choose the preferred one
y = preferTop ? fromBorderRect.top : fromBorderRect.bottom;
res.push({ x, y });
// 2nd routing point
x = fromBorderRect.left;
}
}
else if (toRect.right === toBorderRect.right) {
// to at the right
// Nothing to do
}
else {
// to in between left and right
// eslint-disable-next-line no-lonely-if
if (toRect.top === toBorderRect.top) {
// to at the top, add a point to top right
y = fromBorderRect.top;
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom, add a point to bottom right
y = fromBorderRect.bottom;
}
else {
// Should never be the case, would be hovering somewhere
throw new Error('Invalid case in routeAlongBorder reached.');
}
}
}
else if (fromRect.top === fromBorderRect.top) {
// from at the top
y = fromBorderRect.top;
if (toRect.top === toBorderRect.top) {
// to at the top
// Nothing to do
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom
if (toRect.left === toBorderRect.left) {
// to at the left, add a point to top left
x = fromBorderRect.left;
}
else if (toRect.right === toBorderRect.right) {
// to at the right, add a point to top right
x = fromBorderRect.right;
}
else {
// to in between left and right
// Need 2 routing points, choose the preferred one
x = preferLeft ? fromBorderRect.left : fromBorderRect.right;
res.push({ x, y });
// 2nd routing point
y = fromBorderRect.bottom;
}
}
else {
// to in between top and bottom
// eslint-disable-next-line no-lonely-if
if (toRect.left === toBorderRect.left) {
// to at the left, add a point to top left
x = fromBorderRect.left;
}
else if (toRect.right === toBorderRect.right) {
// to at the right, add a point to top right
x = fromBorderRect.right;
}
else {
// Should never be the case, would be hovering somewhere
throw new Error('Invalid case in routeAlongBorder reached.');
}
}
}
else if (fromRect.bottom === fromBorderRect.bottom) {
// from at the bottom
y = fromBorderRect.bottom;
if (toRect.top === toBorderRect.top) {
// to at the top
if (toRect.left === toBorderRect.left) {
// to at the left, add a point to bottom left
x = fromBorderRect.left;
}
else if (toRect.right === toBorderRect.right) {
// to at the right, add a point to bottom right
x = fromBorderRect.right;
}
else {
// to in between left and right
// Need 2 routing points, choose the preferred one
x = preferLeft ? fromBorderRect.left : fromBorderRect.right;
res.push({ x, y });
// 2nd routing point
y = fromBorderRect.top;
}
}
else if (toRect.bottom === toBorderRect.bottom) {
// to at the bottom
// Nothing to do
}
else {
// to in between top and bottom
// eslint-disable-next-line no-lonely-if
if (toRect.left === toBorderRect.left) {
// to at the left, add a point to top left
x = fromBorderRect.left;
}
else if (toRect.right === toBorderRect.right) {
// to at the right, add a point to top right
x = fromBorderRect.right;
}
else {
// Should never be the case, would be hovering somewhere
throw new Error('Invalid case in routeAlongBorder reached.');
}
}
}
if (x && y) {
// Add the border routing point
res.push({ x, y });
}
return res;
}
Canvas.routeAlongBorder = routeAlongBorder;
/**
* Offsets the canvas by the given values.
* @param canvas The canvas.
* @param offset The offset. Values `>0` reduce the canvas size.
* @returns An offset canvas.
*/
function offsetCanvas(canvas, offset) {
const rectOffset = typeof offset === 'number' ? { left: offset, right: offset, top: offset, bottom: offset } : offset;
const x = canvas.x + rectOffset.left;
const width = canvas.width - rectOffset.right - rectOffset.left;
const y = canvas.y + rectOffset.top;
const height = canvas.height - rectOffset.bottom - rectOffset.top;
return Object.assign(Object.assign({}, canvas), { x, y, width, height });
}
Canvas.offsetCanvas = offsetCanvas;
/// / CRF Functions ////
/**
* Returns the given bounds capped to the canvas border w.r.t. the sidebar if enabled.
* Note that `bounds` and `canvas` need to be in CRF.
* Also, `bounds` has to contain the absolute position (not relative to parent).
* @param bounds The bounds/point to cap to the canvas border, absolute.
* @param canvas The canvas.
* @param capToSidebar Whether the bounds should also be capped to the sidebar.
* @returns The given bounds capped to the canvas border w.r.t. the sidebar if enabled.
*/
function capToCanvas(bounds, canvas, capToSidebar = true) {
var _a, _b;
const originalBounds = asBounds(bounds);
// Cap bounds at canvas border
let x = capNumber(originalBounds.x, canvas.x, canvas.x + canvas.width - originalBounds.width);
const y = capNumber(originalBounds.y, canvas.y, canvas.y + canvas.height - originalBounds.height);
if (capToSidebar) {
// TODO: May be useful to cache the sidebar, since calling document.querySelector()
// can cause overhead if this function is called often
// Make sure the proxies aren't rendered behind the sidebar buttons at the top right
// If the sidebar is locked open, the proxies are also capped to its edge
// TODO: The logic for checking whether proxies should be displayed at all still doesn't
// consider the edge of the sidebar but rather the edge of the viewport which can
// be underneath the sidebar. This should probably be changed in the future.
const rect = (_a = document.querySelector('.sidebar__toggle-container')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
const sidebarRect = (_b = document.querySelector('.sidebar__content')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
const isSidebarOpen = document.querySelector('.sidebar--open');
if (rect) {
if (y < rect.y + rect.height && x > rect.x - originalBounds.width) {
x = rect.x - originalBounds.width;
}
else if (sidebarRect && isSidebarOpen && x > sidebarRect.x - originalBounds.width) {
x = sidebarRect.x - originalBounds.width;
}
}
}
return { x, y, width: originalBounds.width, height: originalBounds.height };
}
Canvas.capToCanvas = capToCanvas;
})(Canvas || (exports.Canvas = Canvas = {}));
var Rect;
(function (Rect) {
/** A Rect with all coordinates as zeros. */
Rect.EMPTY = Object.freeze({ left: 0, right: 0, top: 0, bottom: 0 });
/**
* Returns `bounds` as a Rect.
* @param bounds The Bounds/Dimension to transform into a Rect.
* @returns The Rect corresponding to `bounds`.
*/
function fromBounds(bounds) {
const b = asBounds(bounds);
return { left: b.x, right: b.x + b.width, top: b.y, bottom: b.y + b.height };
}
Rect.fromBounds = fromBounds;
/**
* Returns `rect` as Bounds.
* @param rect The Rect to transform into Bounds.
* @returns The Bounds corresponding to `rect`.
*/
function toBounds(rect) {
return { x: rect.left, y: rect.top, width: rect.right - rect.left, height: rect.bottom - rect.top };
}
Rect.toBounds = toBounds;
})(Rect || (exports.Rect = Rect = {}));
/// ///// Functions ////////
/**
* Checks if this vnode is for a proxy.
* @param vnode The vnode to check this property for.
* @returns If the vnode is for a proxy.
*/
function isProxy(vnode) {
return 'proxy' in vnode;
}
exports.isProxy = isProxy;
/**
* Determines if the SVG element is a proxy rendering for the given node ID.
* @param element The SVG element to check.
* @param nodeId The ID of the node to check.
* @returns if the SVG element is a proxy rendering for the given node ID.
*/
function isProxyRendering(element, nodeId) {
let currentElement = element;
while (currentElement instanceof SVGElement) {
if (currentElement.id.endsWith(nodeId + exports.PROXY_SUFFIX)) {
return true;
}
currentElement = currentElement.parentElement;
}
return false;
}
exports.isProxyRendering = isProxyRendering;
/** Appends {@link PROXY_SUFFIX} to the given id if the given id isn't already a proxy's id. */
function getProxyId(id) {
return id.endsWith(exports.PROXY_SUFFIX) ? id : id + exports.PROXY_SUFFIX;
}
exports.getProxyId = getProxyId;
/** Removes {@link PROXY_SUFFIX} from the given id if the given id is a proxy's id. */
function getNodeId(id) {
return id.endsWith(exports.PROXY_SUFFIX) ? id.substring(0, id.length - exports.PROXY_SUFFIX.length) : id;
}
exports.getNodeId = getNodeId;
/**
* Checks if `b1` is (partially) in `b2`.
* @returns `true` if `b1` is (partially) in `b2`.
*/
function isInBounds(b1, b2) {
const horizontalOverlap = b1.x + b1.width >= b2.x && b1.x <= b2.x + b2.width;
const verticalOverlap = b1.y + b1.height >= b2.y && b1.y <= b2.y + b2.height;
return horizontalOverlap && verticalOverlap;
}
exports.isInBounds = isInBounds;
/**
* Checks if the given bounds overlap.
* @returns `true` if there is overlap.
*/
function checkOverlap(b1, b2) {
return isInBounds(b1, b2) || isInBounds(b2, b1);
}
exports.checkOverlap = checkOverlap;
/**
* Returns `bpd` if given bounds, otherwise fills the empty attributes with zeros.
* @param bpd The bounds/point/dimension.
*/
function asBounds(bpd) {
return (0, sprotty_protocol_1.isBounds)(bpd) ? bpd : Object.assign({ x: 0, y: 0, width: 0, height: 0 }, bpd);
}
exports.asBounds = asBounds;
/**
* Returns `n` capped to the range given by `rangeExtreme1` and `rangeExtreme2`
* (inclusive), e.g. `n` in `[rangeExtreme1, rangeExtreme2]`.
* @param n The number to cap.
* @param rangeExtreme1 The lower bound of the range.
* @param rangeExtreme2 The upper bound of the range.
* @returns `n` capped to the given range. If `rangeExtreme1 > rangeExtreme2`,
* the two are swapped.
*/
function capNumber(n, rangeExtreme1, rangeExtreme2) {
if (rangeExtreme1 > rangeExtreme2) {
return capNumber(n, rangeExtreme2, rangeExtreme1);
}
return Math.max(rangeExtreme1, Math.min(rangeExtreme2, n));
}
exports.capNumber = capNumber;
/**
* Returns the distance between two bounds. If given two points, this
* basically just calculates the euclidean distance between the two.
* Explanation on how the distance is calculated:
*
* Partition the plane into 9 segments (as in tic-tac-toe):
*
* 1 | 2 | 3
*
* --+---+--
*
* 4 | 5 | 6
*
* --+---+--
*
* 7 | 8 | 9
*
* Now 5 correlates to b2.
*
* Using the other segments we can figure out the distance from b1 to b2:
*
* 1,3,7,9: calculate euclidean distance to nearest corner of 5
*
* 2,8: only take y-coordinate into consideration for calculating the distance
*
* 4,6: only take x-coordinate into consideration for calculating the distance
*
* @param bounds1 The first bounds/point to calculate the distance for.
* @param bounds2 The second bounds/point to calculate the distance for.
* @returns The distance between the two bounds.
*/
function distanceBetweenBounds(bounds1, bounds2) {
const b1 = asBounds(bounds1);
const b2 = asBounds(bounds2);
const b1Left = b1.x;
const b1Right = b1Left + b1.width;
const b1Top = b1.y;
const b1Bottom = b1Top + b1.height;
const b2Left = b2.x;
const b2Right = b2Left + b2.width;
const b2Top = b2.y;
const b2Bottom = b2Top + b2.height;
let dist = 0;
if (b1Bottom < b2Top) {
// b1 above b2 (1,2,3)
if (b1Right < b2Left) {
// b1 top left of b2 (1)
dist = sprotty_protocol_1.Point.euclideanDistance({ y: b1Bottom, x: b1Right }, { y: b2Top, x: b2Left });
}
else if (b1Left > b2Right) {
// b1 top right of b2 (3)
dist = sprotty_protocol_1.Point.euclideanDistance({ y: b1Bottom, x: b1Left }, { y: b2Top, x: b2Right });
}
else {
// b1 top middle of b2 (2)
dist = b2Top - b1Bottom;
}
}
else if (b1Top > b2Bottom) {
// b1 below b2 (7,8,9)
if (b1Right < b2Left) {
// b1 bottom left of b2 (7)
dist = sprotty_protocol_1.Point.euclideanDistance({ y: b1Top, x: b1Right }, { y: b2Bottom, x: b2Left });
}
else if (b1Left > b2Right) {
// b1 bottom right of b2 (9)
dist = sprotty_protocol_1.Point.euclideanDistance({ y: b1Top, x: b1Left }, { y: b2Bottom, x: b2Right });
}
else {
// b1 bottom middle of b2 (8)
dist = b1Top - b2Bottom;
}
}
else {
// b1 same height as b2 (4,5,6)
// eslint-disable-next-line no-lonely-if
if (b1Right < b2Left) {
// b1 left of b2 (4)
dist = b2Left - b1Right;
}
else if (b1Left > b2Right) {
// b1 right of b2 (6)
dist = b1Left - b2Right;
}
else {
// b1 on b2 (overlap) (5)
dist = 0;
}
}
return dist;
}
exports.distanceBetweenBounds = distanceBetweenBounds;
/**
* Returns the intersection between the line spanning from `p1` to `p2` and `bounds`.
* @param p1 The start of the line.
* @param p2 The end of the line.
* @param bounds The bounds.
* @returns The intersection between the line and bounds or `undefined` if there is none.
*/
function getIntersection(p1, p2, bounds) {
// Intersection iff one of [p1, p2] in bounds and the other one out of bounds
if (sprotty_protocol_1.Bounds.includes(bounds, p1)) {
// Intersection if p2 out of bounds
if (p2.x < bounds.x || p2.x > bounds.x + bounds.width) {
// Intersection at x, find y
const leftOrRight = p2.x < bounds.x ? bounds.x : bounds.x + bounds.width;
// Scalar of line equation, must be in [0,1] as to not be before p1 or after p2, could be +-inf
const scalar = capNumber((leftOrRight - p1.x) / (p2.x - p1.x), 0, 1);
// Intersection point, cap to canvas with offset (and to sidebar aswell)
const intersectY = p1.y + scalar * (p2.y - p1.y);
return { x: leftOrRight, y: intersectY };
}
if (p2.y < bounds.y || p2.y > bounds.y + bounds.height) {
// Intersection at y, find x
const topOrBottom = p2.y < bounds.y ? bounds.y : bounds.y + bounds.height;
// Scalar of line equation, must be in [0,1] as to not be before p1 or after p2, could be +-inf
const scalar = capNumber((topOrBottom - p1.y) / (p2.y - p1.y), 0, 1);
// Intersection point
const intersectX = p1.x + scalar * (p2.x - p1.x);
return { x: intersectX, y: topOrBottom };
}
}
else if (sprotty_protocol_1.Bounds.includes(bounds, p2)) {
// p1 out of bounds and p2 in bounds, definitely an intersection
if (p1.x < bounds.x || p1.x > bounds.x + bounds.width) {
// Intersection at x, find y
const leftOrRight = p1.x < bounds.x ? bounds.x : bounds.x + bounds.width;
// Scalar of line equation, must be in [0,1] as to not be before p2 or after p1, could be +-inf
const scalar = capNumber((leftOrRight - p2.x) / (p1.x - p2.x), 0, 1);
// Intersection point, cap to canvas with offset (and to sidebar aswell)
const intersectY = p2.y + scalar * (p1.y - p2.y);
return { x: leftOrRight, y: intersectY };
}
// Intersection at y, find x
const topOrBottom = p1.y < bounds.y ? bounds.y : bounds.y + bounds.height;
// Scalar of line equation, must be in [0,1] as to not be before p2 or after p1, could be +-inf
const scalar = capNumber((topOrBottom - p2.y) / (p1.y - p2.y), 0, 1);
// Intersection point
const intersectX = p2.x + scalar * (p1.x - p2.x);
return { x: intersectX, y: topOrBottom };
}
// No intersection
return undefined;
}
exports.getIntersection = getIntersection;
/**
* Updates a VNode's transform attribute.
* @param vnode The VNode.
* @param transform The TransformAttributes.
* @param baseDiv The base div ID of the root node, from the viewerOptions.
*/
function updateTransform(vnode, transform, baseDiv) {
var _a, _b;
// Just changing the VNode's transform attribute is insufficient
// as it doesn't change the document's transform attribute while on the canvas
if (vnode.data) {
if (!vnode.data.attrs) {
vnode.data.attrs = {};
}
let transformString = `translate(${transform.x}, ${transform.y})`;
if (transform.scale) {
transformString += ` scale(${transform.scale})`;
}
if (transform.rotation) {
transformString += ` rotate(${transform.rotation}`;
if (transform.rotationPoint) {
transformString += `, ${transform.rotationPoint.x}, ${transform.rotationPoint.y}`;
}
transformString += ')';
}
// Update transform while off the canvas
vnode.data.attrs.transform = transformString;
// Update transform while on the canvas
(_b = document.getElementById(`${baseDiv}_${(_a = vnode.key) === null || _a === void 0 ? void 0 : _a.toString()}`)) === null || _b === void 0 ? void 0 : _b.setAttribute('transform', transformString);
}
}
exports.updateTransform = updateTransform;
/**
* Updates a VNode's opacity.
* @param vnode The VNode.
* @param opacity The new opacity.
* @param baseDiv The base div ID of the root node, from the viewerOptions.
*/
function updateOpacity(vnode, opacity, baseDiv) {
var _a;
// Just changing the VNode's opacity is insufficient
// as it doesn't change the document's opacity while on the canvas
if (vnode.data) {
if (!vnode.data.style) {
vnode.data.style = {};
}
// Update opacity while off the canvas
vnode.data.style.opacity = opacity.toString();
// Update opacity while on the canvas
const element = document.getElementById(`${baseDiv}_${(_a = vnode.key) === null || _a === void 0 ? void 0 : _a.toString()}`);
if (element) {
element.style.opacity = opacity.toString();
}
}
}
exports.updateOpacity = updateOpacity;
/**
* Updates a VNode's pointer-events to make it click-through.
* @param vnode The VNode.
* @param clickThrough Whether the VNode should be click-through.
* @param baseDiv The base div ID of the root node, from the viewerOptions.
*/
function updateClickThrough(vnode, clickThrough, baseDiv) {
var _a;
// Just changing the VNode's pointer-events is insufficient
// as it doesn't change the document's pointer-events while on the canvas
if (vnode.data) {
if (!vnode.data.style) {
vnode.data.style = {};
}
const pointerEvent = clickThrough ? 'none' : 'auto';
// Update pointer-events while off the canvas
vnode.data.style['pointer-events'] = pointerEvent;
// Update pointer-events while on the canvas
const element = document.getElementById(`${baseDiv}_${(_a = vnode.key) === null || _a === void 0 ? void 0 : _a.toString()}`);
if (element) {
element.style.pointerEvents = pointerEvent;
}
}
}
exports.updateClickThrough = updateClickThrough;
/**
* Checks if `item` is contained in any (nested) group, e.g. if `item` is contained in a flattened version of `groups`.
* @returns `true` if `item` is contained in any (nested) group.
* @example anyContains([[0, 1], [1, 2]], 2) === true
*/
function anyContains(groups, item) {
return groups.reduce((acc, group) => acc.concat(group), []).includes(item);
}
exports.anyContains = anyContains;
/**
* Join groups containing at least 1 same element. Transitive joining applies:
* @example joinTransitiveGroups([[0, 1], [1, 2], [2, 3]]) === [[0, 1, 2, 3]]
*/
function joinTransitiveGroups(groups) {
const res = [];
while (groups.length > 0) {
// Use a set for removing duplicates
let firstGroup = Array.from(new Set(groups.shift()));
let remainingGroups = [...groups];
let prevLength = -1;
while (firstGroup.length > prevLength) {
// Iterate until no group can be joined with firstGroup anymore
prevLength = firstGroup.length;
const nextRemainingGroups = [];
for (const group of remainingGroups) {
if (new Set([...firstGroup].filter((x) => group.includes(x))).size > 0) {
// Intersection of firstGroup and group is not empty, join both groups
firstGroup = Array.from(new Set(firstGroup.concat(group)));
}
else {
// firstGroup and group share no element
nextRemainingGroups.push(group);
}
}
remainingGroups = nextRemainingGroups;
}
// firstGroup has been fully joined, add to res and continue with remainingGroups
res.push(Array.from(new Set(firstGroup)));
groups = remainingGroups;
}
return res;
}
exports.joinTransitiveGroups = joinTransitiveGroups;
/**
* Checks if `node` has an incoming edge to at least one of the other given `nodes`.
* @returns `true` if `node` has an incoming edge to at least one of the other given `nodes`.
*/
function isIncomingToAny(node, nodes) {
const ids = nodes.map((theNode) => theNode.id);
return ids.length > 0 && node.incomingEdges.some((edge) => ids.includes(edge.sourceId));
}
exports.isIncomingToAny = isIncomingToAny;
/**
* Checks if `node` has an outgoing edge to at least one of the other given `nodes`.
* @returns `true` if `node` has an outgoing edge to at least one of the other given `nodes`.
*/
function isOutgoingToAny(node, nodes) {
const ids = nodes.map((theNode) => theNode.id);
return ids.length > 0 && node.outgoingEdges.some((edge) => ids.includes(edge.targetId));
}
exports.isOutgoingToAny = isOutgoingToAny;
/**
* Checks if `node` is connected to at least one of the other given `nodes`.
* @returns `true` if `node` is connected to at least one of the other given `nodes`.
*/
function isConnectedToAny(node, nodes) {
return isIncomingToAny(node, nodes) || isOutgoingToAny(node, nodes);
}
exports.isConnectedToAny = isConnectedToAny;
/** Checks if `node` is selected or connected to any selected element. */
function isSelectedOrConnectedToSelected(node) {
const selectedNodes = SelectedElementsUtil.getSelectedNodes();
return node.selected || isConnectedToAny(node, selectedNodes);
}
exports.isSelectedOrConnectedToSelected = isSelectedOrConnectedToSelected;
/// ///// Classes ////////
/** Util class for easily accessing the currently selected elements. */
class SelectedElementsUtil {
/**
* Clears all caches for stored element types.
*/
static clearCaches() {
this.nodeCache = undefined;
this.edgeCache = undefined;
this.labelCache = undefined;
this.portCache = undefined;
}
/**
* Recalculates the selected elements.
*/
static recalculateSelection() {
var _a;
this.clearCaches();
this.selectedElements = [];
(_a = this.index) === null || _a === void 0 ? void 0 : _a.all().forEach((element) => {
if ((0, sprotty_1.isSelectable)(element) && element.selected && (0, skgraph_models_1.isSKGraphElement)(element)) {
this.selectedElements.push(element);
}
});
}
/** Checks if the current index is reset. */
static isReset() {
return this.index === undefined;
}
/** Resets the current index elements. */
static resetModel() {
this.index = undefined;
this.selectedElements = [];
}
/** Sets the current root. */
static setRoot(root) {
// this.currRoot = root;
this.index = new sprotty_1.ModelIndexImpl();
this.index.add(root);
// calculate the selected elements.
this.recalculateSelection();
}
/// / Util methods ////
/** Returns the currently selected elements. */
static getSelectedElements() {
return this.selectedElements;
}
/** Returns whether there are any currently selected elements. */
static areElementsSelected() {
return this.getSelectedElements().length > 0;
}
/** Returns the currently selected nodes. */
static getSelectedNodes() {
var _a;
this.nodeCache = (_a = this.nodeCache) !== null && _a !== void 0 ? _a : this.selectedElements.filter((node) => node instanceof skgraph_models_1.SKNode);
return this.nodeCache;
}
/** Returns whether there are any currently selected nodes. */
static areNodesSelected() {
return this.getSelectedNodes().length > 0;
}
/** Returns the currently selected edges. */
static getSelectedEdges() {
var _a;
this.edgeCache = (_a = this.edgeCache) !== null && _a !== void 0 ? _a : this.selectedElements.filter((node) => node instanceof skgraph_models_1.SKEdge);
return this.edgeCache;
}
/** Returns whether there are any currently selected edges. */
static areEdgesSelected() {
return this.getSelectedEdges().length > 0;
}
/** Returns the currently selected labels. */
static getSelectedLabels() {
var _a;
this.labelCache =
(_a = this.labelCache) !== null && _a !== void 0 ? _a : this.selectedElements.filter((node) => node instanceof skgraph_models_1.SKLabel);
return this.labelCache;
}
/** Returns whether there are any currently selected labels. */
static areLabelsSelected() {
return this.getSelectedLabels().length > 0;
}
/** Returns the currently selected ports. */
static getSelectedPorts() {
var _a;
this.portCache = (_a = this.portCache) !== null && _a !== void 0 ? _a : this.selectedElements.filter((node) => node instanceof skgraph_models_1.SKPort);
return this.portCache;
}
/** Returns whether there are any currently selected ports. */
static arePortsSelected() {
return this.getSelectedPorts().length > 0;
}
}
exports.SelectedElementsUtil = SelectedElementsUtil;
/** Handles all actions regarding the {@link SelectedElementsUtil}. */
let SelectedElementsUtilActionHandler = class SelectedElementsUtilActionHandler {
handle(action) {
if (action.kind === sprotty_protocol_1.SetModelAction.KIND || action.kind === sprotty_protocol_1.UpdateModelAction.KIND) {
// Reset
SelectedElementsUtil.resetModel();
}
else if (action.kind === actions_1.SendModelContextAction.KIND) {
SelectedElementsUtil.recalculateSelection();
if (SelectedElementsUtil.isReset()) {
// Set new root
const sMCAction = action;
SelectedElementsUtil.setRoot(sMCAction.model.root);
}
}
}
initialize(registry) {
// New model
registry.register(sprotty_protocol_1.SetModelAction.KIND, this);
registry.register(sprotty_protocol_1.UpdateModelAction.KIND, this);
registry.register(actions_1.SendModelContextAction.KIND, this);
}
};
exports.SelectedElementsUtilActionHandler = SelectedElementsUtilActionHandler;
exports.SelectedElementsUtilActionHandler = SelectedElementsUtilActionHandler = __decorate([
(0, inversify_1.injectable)()
], SelectedElementsUtilActionHandler);
//# sourceMappingURL=proxy-view-util.js.map