UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

141 lines (140 loc) 4.84 kB
import { computed, signal } from "@preact/signals-core"; import { GraphComponent } from "../../../components/canvas/GraphComponent"; import { ESchedulerPriority } from "../../../lib"; import { vectorDistance } from "../../../utils/functions"; import { Point } from "../../../utils/types/shapes"; import { debounce } from "../../../utils/utils/schedule"; import { PortState } from "./Port"; export class PortsStore { constructor(rootStore, graph) { this.rootStore = rootStore; this.graph = graph; this.$ports = computed(() => { return Array.from(this.$portsMap.value.values()); }); this.$portsMap = signal(new Map()); this.notifyPortMapChanged = debounce(() => { this.$portsMap.value = new Map(this.$portsMap.value); }, { priority: ESchedulerPriority.LOW, frameInterval: 1, }); } createPort(id, component) { if (this.$portsMap.value.has(id)) { const existingPort = this.$portsMap.value.get(id); if (existingPort) { existingPort.setOwner(component); return existingPort; } } const newPort = new PortState({ id, x: 0, y: 0, component, lookup: !component, }); this.$portsMap.value.set(id, newPort); this.notifyPortMapChanged(); return newPort; } getPort(id) { return this.$portsMap.value.get(id); } getOrCreatePort(id, component) { const existingPort = this.getPort(id); if (existingPort) { if (component && !existingPort.owner) { existingPort.setOwner(component); } return existingPort; } return this.createPort(id, component); } deletePort(id) { const deleted = this.$portsMap.value.delete(id); if (deleted) { this.notifyPortMapChanged(); } return deleted; } deletePorts(ids) { ids.forEach((id) => { this.$portsMap.value.delete(id); }); } clearPorts() { this.$portsMap.value.clear(); this.notifyPortMapChanged(); } getPortsByComponent(component) { return this.$ports.value.filter((port) => port.component === component); } ownPort(port, component) { port.addObserver(component); } unownPort(port, component) { port.removeObserver(component); if (port.observers.size === 0 && !port.component) { this.deletePort(port.id); } } reset() { this.clearPorts(); } /** * Find the nearest port at given world coordinates * First finds components under cursor, then checks their ports * * @param point World coordinates to search from * @param searchRadius Maximum search radius in pixels (default: 0 - exact match) * @param filter Optional filter function to validate ports * @returns Nearest port within radius, or undefined if none found * * @example * ```typescript * const port = portsStore.findPortAtPoint( * { x: 100, y: 200 }, * 30, * (port) => !port.lookup && port.owner !== undefined * ); * ``` */ findPortAtPoint(point, searchRadius = 0, filter) { // Get all components under cursor (GraphComponent only) const pointObj = new Point(point.x, point.y); const component = this.graph.getElementOverPoint(pointObj, [GraphComponent]); if (!component) { return undefined; } return this.findPortAtPointByComponent(component, point, searchRadius, filter); } /** * Find the nearest port at given world coordinates for a specific component * * @param component Component to search in * @param point World coordinates to search from * @param searchRadius Maximum search radius in pixels (default: 0 - exact match) * @param filter Optional filter function to validate ports * @returns Nearest port within radius, or undefined if none found */ findPortAtPointByComponent(component, point, searchRadius = 0, filter) { const ports = component.getPorts(); for (const port of ports) { // Skip ports in lookup state (no valid coordinates) if (port.lookup) continue; // Calculate vector distance const distance = vectorDistance(point, port); // Check if within radius and closer than previous if (distance <= searchRadius) { // Apply optional filter if (filter && !filter(port)) continue; return port; } } return undefined; } }