@finos/legend-extension-dsl-diagram
Version:
Legend extension for Diagram DSL
301 lines (271 loc) • 9.75 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RelationshipView } from '../metamodel/pure/packageableElements/diagram/DSL_Diagram_RelationshipView.js';
import { Point } from '../metamodel/pure/packageableElements/diagram/geometry/DSL_Diagram_Point.js';
import {
type PureModel,
Class,
getAllOwnClassProperties,
} from '@finos/legend-graph';
import { deleteEntry, guaranteeNonNullable } from '@finos/legend-shared';
import type { ClassView } from '../metamodel/pure/packageableElements/diagram/DSL_Diagram_ClassView.js';
import type { Diagram } from '../metamodel/pure/packageableElements/diagram/DSL_Diagram_Diagram.js';
import { PositionedRectangle } from '../metamodel/pure/packageableElements/diagram/geometry/DSL_Diagram_PositionedRectangle.js';
import { Rectangle } from '../metamodel/pure/packageableElements/diagram/geometry/DSL_Diagram_Rectangle.js';
export class Vector {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
static fromPoints(a: Point, b: Point): Vector {
return new Vector(b.x - a.x, b.y - a.y);
}
unit(): Vector {
const norm = Math.sqrt(this.x * this.x + this.y * this.y);
return new Vector(this.x / norm, this.y / norm);
}
normal(other: Vector): Vector {
return new Vector(other.y - this.y, -(other.x - this.x));
}
dotProduct(other: Vector): number {
return this.x * other.x + this.y * other.y;
}
}
/**
* Get absolute position of element on the screen by recursively walking up element tree
*/
export const getElementPosition = (element: HTMLElement): Point => {
let xPosition = 0;
let yPosition = 0;
while (element.offsetParent) {
xPosition += element.offsetLeft - element.scrollLeft + element.clientLeft;
yPosition += element.offsetTop - element.scrollTop + element.clientTop;
element = element.offsetParent as HTMLElement;
}
return new Point(xPosition, yPosition);
};
export const getClassView = (
diagram: Diagram,
sourceViewId: string,
): ClassView | undefined =>
diagram.classViews.find((classView) => classView.id === sourceViewId);
export const cleanUpDeadReferencesInDiagram = (
diagram: Diagram,
graph: PureModel,
): void => {
// Delete orphan property views
const propertyViewsToRemove = diagram.propertyViews.filter(
(propertyView) =>
!(
propertyView.property.ownerReference.value instanceof Class
? getAllOwnClassProperties(propertyView.property.ownerReference.value)
: propertyView.property.ownerReference.value.properties
)
.map((property) => property.name)
.includes(propertyView.property.value.name),
);
propertyViewsToRemove.forEach((propertyView) =>
deleteEntry(diagram.propertyViews, propertyView),
);
// Fix orphan class views
const classViewsToRemove = diagram.classViews.filter(
(cv) => !graph.getNullableClass(cv.class.value.path),
);
classViewsToRemove.forEach((cw) => deleteEntry(diagram.classViews, cw));
// Fix orphan gneralization views
const generalizationViewsToRemove = diagram.generalizationViews.filter(
(g) => {
const srcClass = g.from.classView.value.class.value;
const targetClass = g.to.classView.value.class.value;
return (
!graph.getNullableClass(srcClass.path) ||
!graph.getNullableClass(targetClass.path) ||
srcClass.generalizations.filter((c) => c.value.rawType === targetClass)
.length === 0
);
},
);
generalizationViewsToRemove.forEach((g) =>
deleteEntry(diagram.generalizationViews, g),
);
};
export const _relationshipView_setPath = (
relationshipView: RelationshipView,
val: Point[],
): void => {
relationshipView.path = val;
};
/**
* Simplify the path.
*
* Flatten the path if the angle is wide enough between 3 consecutive points
* Also remove unnecessary inside points
*/
export const _relationshipView_simplifyPath = (
relationshipView: RelationshipView,
): void => {
const fullPath = relationshipView.buildFullPath();
// NOTE: this method here will `swallow` up points inside of the boxes
const newPath = RelationshipView.pruneUnnecessaryInsidePoints(
fullPath,
relationshipView.from.classView.value,
relationshipView.to.classView.value,
);
// recompute the offset point from center inside of `from` and `to` classviews.
// for each, we first check if `manageInsidePointsDynamically` removes any points from the full path
// if it does we will update the offset
if (newPath[0] !== fullPath[0]) {
const center = relationshipView.from.classView.value.center();
relationshipView.from._offsetX =
guaranteeNonNullable(
newPath[0],
'Diagram path expected to have at least 2 points',
).x - center.x;
relationshipView.from._offsetY =
guaranteeNonNullable(
newPath[0],
'Diagram path expected to have at least 2 points',
).y - center.y;
}
if (newPath[newPath.length - 1] !== fullPath[fullPath.length - 1]) {
const center = relationshipView.to.classView.value.center();
relationshipView.to._offsetX =
(newPath[newPath.length - 1] as Point).x - center.x;
relationshipView.to._offsetY =
(newPath[newPath.length - 1] as Point).y - center.y;
}
// find the point which can be flattened due to its wide angle
const result = [];
for (let i = 0; i < newPath.length - 2; i++) {
const v1 = Vector.fromPoints(
newPath[i + 1] as Point,
newPath[i] as Point,
).unit();
const v2 = Vector.fromPoints(
newPath[i + 1] as Point,
newPath[i + 2] as Point,
).unit();
const dot = v1.dotProduct(v2);
const angle = (Math.acos(dot) * 180) / Math.PI;
if (Math.abs(angle - 180) > 5) {
result.push(newPath[i + 1] as Point);
}
}
// NOTE: this new path does not contain the 2 end points
_relationshipView_setPath(relationshipView, result);
};
/**
* Based on the location, find the point on the path that matches or create new point
* (within a threshold of proximity) from the coordinate and put this in the path array
* so it doesn't look too weird
*/
export const _findOrBuildPoint = (
relationshipView: RelationshipView,
x: number,
y: number,
zoom: number,
allowChange: boolean,
): Point | undefined => {
for (const pt of relationshipView.path) {
if (
Math.sqrt((x - pt.x) * (x - pt.x) + (y - pt.y) * (y - pt.y)) <
10 / zoom
) {
return pt;
}
}
const fullPath = relationshipView.buildFullPath(allowChange);
const newPath = [];
let point;
for (let i = 0; i < fullPath.length - 1; i++) {
const a = fullPath[i] as Point;
const b = fullPath[i + 1] as Point;
const u = new Vector(a.x, a.y).normal(new Vector(b.x, b.y)).unit();
const v = Vector.fromPoints(a, new Point(x, y));
// if the selection point is not too far from the segment
// of the path, create a new point and make it part of the path
if (Math.abs(u.dotProduct(v)) < 5 / zoom) {
const lx = (a.x < b.x ? a.x : b.x) - 5 / zoom;
const hx = (a.x < b.x ? b.x : a.x) + 5 / zoom;
const ly = (a.y < b.y ? a.y : b.y) - 5 / zoom;
const hy = (a.y < b.y ? b.y : a.y) + 5 / zoom;
if (lx <= x && x <= hx && ly <= y && y <= hy) {
point = new Point(x, y);
newPath.push(point);
}
}
if (i < fullPath.length - 2) {
newPath.push(fullPath[i + 1] as Point);
}
}
if (point && allowChange) {
// NOTE: this new path does not contain the 2 end points
_relationshipView_setPath(relationshipView, newPath);
}
return point;
};
export const rotatePointX = (point: Point, angle: number): number =>
point.x * Math.cos(angle) - point.y * Math.sin(angle);
export const rotatePointY = (point: Point, angle: number): number =>
point.x * Math.sin(angle) + point.y * Math.cos(angle);
export const getBottomRightCornerPoint = (pR: PositionedRectangle): Point =>
new Point(
pR.position.x + pR.rectangle.width,
pR.position.y + pR.rectangle.height,
);
/**
* Build a small box at the bottom right corner of the rectangle so we
* can use that for selection to resize the box
*/
export const buildBottomRightCornerBox = (
pR: PositionedRectangle,
): PositionedRectangle => {
const bottomRightCornerPoint = getBottomRightCornerPoint(pR);
const boxSize = 10;
return new PositionedRectangle(
new Point(
bottomRightCornerPoint.x - boxSize / 2,
bottomRightCornerPoint.y - boxSize / 2,
),
new Rectangle(boxSize, boxSize),
);
};
/**
* Check if a box contains another box
*/
export const boxContains = (
box: PositionedRectangle,
otherBox: PositionedRectangle,
): boolean => {
otherBox = box.normalizeBox(otherBox);
return (
box.contains(otherBox.position.x, otherBox.position.y) ||
box.contains(
otherBox.position.x + otherBox.rectangle.width,
otherBox.position.y,
) ||
box.contains(
otherBox.position.x,
otherBox.position.y + otherBox.rectangle.height,
) ||
box.contains(
otherBox.position.x + otherBox.rectangle.width,
otherBox.position.y + otherBox.rectangle.height,
)
);
};