@gravity-ui/graph
Version:
Modern graph editor component
180 lines (179 loc) • 7.51 kB
JavaScript
import { batch } from "@preact/signals-core";
import { ESelectionStrategy } from "../services/selection/types";
import { selectBlockById } from "../store/block/selectors";
import { selectConnectionById } from "../store/connection/selectors";
import { getBlocksRect, getElementsRect, startAnimation } from "../utils/functions";
export class PublicGraphApi {
constructor(graph) {
this.graph = graph;
// noop
}
/**
* Zooms to blocks
* @param blockIds - block ids to zoom to
* @param zoomConfig - {@link ZoomConfig} zoom config
* @returns {boolean} true if zoom is successful, false otherwise
*
* @example
* ```typescript
* graph.zoomToBlocks([block1.id, block2.id]);
* graph.zoomToBlocks([block1.id, block2.id], { transition: 1000, padding: 50 });
* ```
*/
zoomToBlocks(blockIds, zoomConfig) {
const blocks = this.graph.rootStore.blocksList.getBlocks(blockIds);
if (blocks.length === 0) {
return false;
}
const blocksRect = getBlocksRect(blocks);
this.zoomToRect(blocksRect, zoomConfig);
return true;
}
/**
* Zooms to GraphComponent instances
* @param instances - {@link GraphComponent} instances to zoom to
* @param zoomConfig - {@link ZoomConfig} zoom config
* @returns {boolean} true if zoom is successful, false otherwise
*
* @example
* ```typescript
* graph.zoomToElements([component1, component2]);
* graph.zoomToElements([component1, component2], { transition: 1000, padding: 50 });
* ```
*/
zoomToElements(elements, zoomConfig) {
if (elements.length === 0) {
return false;
}
const elementsRect = getElementsRect(elements);
this.zoomToRect(elementsRect, zoomConfig);
return true;
}
/**
* Zooms to fit all blocks in the viewport. This method is asynchronous and waits
* for the usableRect to be ready before performing the zoom operation.
*
* @param zoomConfig - Configuration for zoom transition and padding
* @returns Promise that resolves when zoom operation is complete
*/
zoomToViewPort(zoomConfig) {
this.graph.hitTest.waitUsableRectUpdate((rect) => {
let zoomRect = rect;
// if rect is empty we need apply usable rect gap
// or zoom to center will be applied at position {0,0} with width/height 0/0
if (rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0) {
zoomRect = {
x: 0 - this.graph.graphConstants.system.USABLE_RECT_GAP,
y: 0 - this.graph.graphConstants.system.USABLE_RECT_GAP,
width: 0 + this.graph.graphConstants.system.USABLE_RECT_GAP * 2,
height: 0 + this.graph.graphConstants.system.USABLE_RECT_GAP * 2,
};
}
this.zoomToRect(zoomRect, zoomConfig);
});
}
/**
* Zooms to the specified rectangle in camera coordinates
*
* Zooms the camera to fit the specified rectangle in the viewport. The rectangle will be scaled
* to fill the visible area (respecting camera insets) and centered in the viewport.
*
* @param rect - {@link TRect} rectangle to zoom to in camera coordinates
* @param zoomConfig - {@link ZoomConfig} zoom config
* @returns {undefined}
*
* @example
* ```typescript
* graph.zoomToRect({ x: 0, y: 0, width: 100, height: 100 });
* graph.zoomToRect({ x: 0, y: 0, width: 100, height: 100 }, { transition: 1000, padding: 50 });
*/
zoomToRect(rect, zoomConfig) {
const transition = zoomConfig?.transition || 0;
const padding = zoomConfig?.padding || 0;
const cameraRectInit = this.graph.cameraService.getCameraRect();
const cameraScaleInit = this.graph.cameraService.getCameraScale();
// Compute scale against visible viewport (respectInsets) so content fits actual visible area
const endScale = this.graph.cameraService.getScaleRelativeDimensions(rect.width + padding * 2, rect.height + padding * 2, { respectInsets: true });
const xyPosition = this.graph.cameraService.getXYRelativeCenterDimensions({
x: rect.x - padding,
y: rect.y - padding,
width: rect.width + padding * 2,
height: rect.height + padding * 2,
}, endScale, { respectInsets: true });
if (!transition) {
this.graph.cameraService.set({ ...xyPosition, scale: endScale });
return;
}
startAnimation(transition, (progress) => {
const x = cameraRectInit.x + (xyPosition.x - cameraRectInit.x) * progress;
const y = cameraRectInit.y + (xyPosition.y - cameraRectInit.y) * progress;
const scale = cameraScaleInit + (endScale - cameraScaleInit) * progress;
this.graph.cameraService.set({ x, y, scale });
});
}
getGraphColors() {
return this.graph.graphColors;
}
updateGraphColors(colors) {
this.graph.setColors(colors);
}
getGraphConstants() {
return this.graph.graphConstants;
}
updateGraphConstants(constants) {
this.graph.setConstants(constants);
}
isGraphEmpty() {
return this.graph.rootStore.blocksList.$blocksMap.value.size === 0;
}
setSetting(flagPath, value) {
this.graph.rootStore.settings.setConfigFlag(flagPath, value);
}
setCurrentConfigurationName(newName) {
this.graph.rootStore.configurationName = newName;
}
deleteSelected() {
batch(() => {
this.graph.rootStore.connectionsList.deleteSelectedConnections();
this.graph.rootStore.blocksList.deleteSelectedBlocks();
});
}
selectBlocks(blockIds, selected, strategy = ESelectionStrategy.REPLACE) {
this.graph.rootStore.blocksList.updateBlocksSelection(blockIds, selected, strategy);
}
updateBlock(block) {
const blockStore = selectBlockById(this.graph, block.id);
blockStore?.updateBlock(block);
}
addBlock(block, selectionOptions) {
const newBlockId = this.graph.rootStore.blocksList.addBlock(block);
if (selectionOptions !== undefined) {
this.graph.rootStore.blocksList.updateBlocksSelection([newBlockId], selectionOptions.selected !== undefined ? selectionOptions.selected : true, selectionOptions.strategy);
}
return newBlockId;
}
setAnchorSelection(blockId, anchorId, selected) {
this.graph.rootStore.blocksList.setAnchorSelection(blockId, anchorId, selected);
}
selectConnections(connectionIds, selected, strategy = ESelectionStrategy.REPLACE) {
batch(() => {
this.graph.rootStore.connectionsList.setConnectionsSelection(connectionIds, selected, strategy);
});
}
updateConnection(id, connection) {
const connectionStore = selectConnectionById(this.graph, id);
connectionStore.updateConnection(connection);
}
addConnection(connection) {
return this.graph.rootStore.connectionsList.addConnection(connection);
}
getBlockById(blockId) {
return selectBlockById(this.graph, blockId)?.asTBlock();
}
getUsableRect() {
return this.graph.hitTest.getUsableRect();
}
unsetSelection() {
this.graph.rootStore.selectionService.resetAllSelections();
}
}