UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

201 lines (200 loc) 7.53 kB
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";