@gravity-ui/graph
Version:
Modern graph editor component
201 lines (200 loc) • 7.53 kB
JavaScript
import { ECanDrag } from "../../store/settings";
import { SELECTION_EVENT_TYPES } from "../types/events";
import { Rect } from "../types/shapes";
export { parseClassNames } from "./classNames";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function noop(...args) {
// noop
}
export function isTouchEvent(event) {
return globalThis.TouchEvent ? event instanceof globalThis.TouchEvent : event.type?.startsWith("touch");
}
export function getXY(root, event) {
if (!("pageX" in event))
return [-1, -1];
const rect = root.getBoundingClientRect();
return [event.pageX - rect.left - window.scrollX, event.pageY - rect.top - window.scrollY];
}
export function getCoord(event, coord) {
const name = `page${coord.toUpperCase()}`;
if (isTouchEvent(event)) {
return event.touches[0][name];
}
return event[name];
}
export function getEventDelta(e1, e2) {
return Math.abs(getCoord(e1, "x") - getCoord(e2, "x")) + Math.abs(getCoord(e1, "y") - getCoord(e2, "y"));
}
export function isMetaKeyEvent(event) {
return event.metaKey || event.ctrlKey;
}
export function isShiftKeyEvent(event) {
return event.shiftKey;
}
export function isAltKeyEvent(event) {
return event.altKey;
}
export function getEventSelectionAction(event) {
if (isMetaKeyEvent(event))
return SELECTION_EVENT_TYPES.TOGGLE;
return SELECTION_EVENT_TYPES.DELETE;
}
export function isBlock(component) {
return component?.isBlock;
}
export function createObject(simpleObject, forDefineProperties) {
const defaultProperties = {
configurable: true,
enumerable: true,
};
const keys = Object.keys(forDefineProperties);
for (let i = 0; i < keys.length; i += 1) {
forDefineProperties[keys[i]] = { ...defaultProperties, ...forDefineProperties[keys[i]] };
}
Object.defineProperties(simpleObject, forDefineProperties);
return simpleObject;
}
export function addEventListeners(instance, mapEventsToFn) {
if (mapEventsToFn === undefined)
return noop;
const subs = [];
const events = Object.keys(mapEventsToFn);
for (let i = 0; i < events.length; i += 1) {
instance.addEventListener(events[i], mapEventsToFn[events[i]].bind(instance));
subs.push(instance.removeEventListener.bind(instance, events[i], mapEventsToFn[events[i]]));
}
return () => subs.forEach((f) => f());
}
/**
* Check if drag is allowed based on canDrag setting and component selection state.
* @param canDrag - The canDrag setting value
* @param isSelected - Whether the component is currently selected
* @returns true if the component can be dragged
*/
export function isAllowDrag(canDrag, isSelected) {
if (canDrag === ECanDrag.ALL)
return true;
return canDrag === ECanDrag.ONLY_SELECTED && isSelected;
}
/**
* Gets the usable rectangle that encompasses the specified blocks.
* If no blocks are provided or the blocks array is empty, returns a default rectangle (0,0,0,0)
* to prevent camera state issues with Infinity values.
*
* @param blocks - Array of blocks to calculate bounding box for
* @returns TRect representing the bounding box of the blocks, or a default rect (0,0,0,0) if no blocks or invalid geometry
*/
export function getBlocksRect(blocks) {
// If no blocks or all blocks are not found, return a default rectangle to prevent camera state issues with Infinity values.
if (blocks.length === 0) {
return new Rect(0, 0, 0, 0);
}
const geometry = blocks.reduce((acc, item) => {
acc.minX = Math.min(acc.minX, item.x);
acc.minY = Math.min(acc.minY, item.y);
acc.maxX = Math.max(acc.maxX, item.x + item.width);
acc.maxY = Math.max(acc.maxY, item.y + item.height);
return acc;
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
const rect = new Rect(geometry.minX, geometry.minY, geometry.maxX - geometry.minX, geometry.maxY - geometry.minY);
if (isGeometryHaveInfinity(rect)) {
return new Rect(0, 0, 0, 0);
}
return rect;
}
export function getElementsRect(elements) {
if (elements.length === 0) {
return new Rect(0, 0, 0, 0);
}
const elementsRect = elements.reduce((acc, item) => {
const [x, y, width, height] = item.getHitBox();
acc.minX = Math.min(acc.minX, x);
acc.minY = Math.min(acc.minY, y);
acc.maxX = Math.max(acc.maxX, x + width);
acc.maxY = Math.max(acc.maxY, y + height);
return acc;
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
return new Rect(elementsRect.minX, elementsRect.minY, elementsRect.maxX - elementsRect.minX, elementsRect.maxY - elementsRect.minY);
}
export function isGeometryHaveInfinity(geometry) {
let infinityHave = false;
Object.entries(geometry).forEach((entry) => {
if (!isFinite(entry[1]))
infinityHave = true;
});
return infinityHave;
}
export function startAnimation(duration, draw) {
const start = performance.now();
requestAnimationFrame(function animate(time) {
let progress = (time - start) / duration;
if (progress > 1)
progress = 1;
draw(progress);
if (progress < 1) {
requestAnimationFrame(animate);
}
});
}
/**
* Calculates a "nice" number approximately equal to the range.
* Useful for determining tick spacing on axes or rulers.
* Algorithm adapted from "Nice Numbers for Graph Labels" by Paul Heckbert
* @param range The desired approximate range or step.
* @param round Whether to round the result (usually false for step calculation).
* @returns A nice number (e.g., 1, 2, 5, 10, 20, 50, ...).
*/
export function calculateNiceNumber(range, round = false) {
if (range <= 0) {
return 0;
}
const exponent = Math.floor(Math.log10(range));
const fraction = range / 10 ** exponent;
let niceFraction;
if (round) {
if (fraction < 1.5)
niceFraction = 1;
else if (fraction < 3)
niceFraction = 2;
else if (fraction < 7)
niceFraction = 5;
else
niceFraction = 10;
}
else if (fraction <= 1)
niceFraction = 1;
else if (fraction <= 2)
niceFraction = 2;
else if (fraction <= 5)
niceFraction = 5;
else
niceFraction = 10;
return niceFraction * 10 ** exponent;
}
/**
* Aligns a coordinate value to the device's physical pixel grid for sharper rendering.
* @param value The coordinate value (e.g., x or y).
* @param dpr The device pixel ratio.
* @returns The aligned coordinate value.
*/
export function alignToPixelGrid(value, dpr) {
// Scale by DPR, round to the nearest integer (physical pixel), then scale back.
// Add 0.001 to prevent floating point issues where rounding might go down unexpectedly.
return Math.round(value * dpr + 0.001) / dpr;
}
export function computeCssVariable(name) {
if (!name.startsWith("var("))
return name;
const body = globalThis.document.body;
if (!body)
return name;
const computedStyle = window.getComputedStyle(body);
if (!computedStyle)
return name;
name = name.substring(4);
name = name.substring(0, name.length - 1);
return computedStyle.getPropertyValue(name).trim();
}
// Re-export scheduler utilities
export { schedule, debounce, throttle } from "../utils/schedule";
export { isTrackpadWheelEvent } from "./isTrackpadDetector";