@gravity-ui/graph
Version:
Modern graph editor component
141 lines (140 loc) • 4.84 kB
JavaScript
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;
}
}