UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

132 lines (131 loc) 5.83 kB
import { extractNativeGraphMouseEvent } from "../../../../graphEvents"; import { Layer } from "../../../../services/Layer"; import { Camera } from "../../../../services/camera/Camera"; import { ESelectionStrategy } from "../../../../services/selection"; import { isMetaKeyEvent } from "../../../../utils/functions"; import { render } from "../../../../utils/renderers/render"; function getSelectionRect(sx, sy, ex, ey) { if (sx > ex) [sx, ex] = [ex, sx]; if (sy > ey) [sy, ey] = [ey, sy]; return [sx, sy, ex - sx, ey - sy]; } export class SelectionLayer extends Layer { constructor(props) { super({ canvas: { zIndex: 4, classNames: ["no-pointer-events"], transformByCameraPosition: true, // Automatically apply camera transformation ...props.canvas, }, ...props, }); // Store selection in world coordinates to handle auto-panning correctly this.selectionStartWorld = null; this.selectionEndWorld = null; this.handleMouseDown = (nativeEvent) => { if (!this.root?.ownerDocument) { return; } const event = extractNativeGraphMouseEvent(nativeEvent); const target = nativeEvent.detail.target; // If target is camera, that means that user is start selection outside any elements if (!(target instanceof Camera)) { return; } if (event && isMetaKeyEvent(event)) { nativeEvent.preventDefault(); nativeEvent.stopPropagation(); this.context.graph.dragService.startDrag({ onStart: this.startSelectionRender, onUpdate: this.updateSelectionRender, onEnd: this.endSelectionRender, }); } }; this.updateSelectionRender = (event, [worldX, worldY]) => { this.selectionEndWorld = { x: worldX, y: worldY }; this.performRender(); }; this.startSelectionRender = (_event, [worldX, worldY]) => { this.selectionStartWorld = { x: worldX, y: worldY }; this.selectionEndWorld = { x: worldX, y: worldY }; }; this.endSelectionRender = (_event) => { if (!this.selectionStartWorld || !this.selectionEndWorld) { return; } // Check if selection has any size const hasSizeX = Math.abs(this.selectionEndWorld.x - this.selectionStartWorld.x) > 0.1; const hasSizeY = Math.abs(this.selectionEndWorld.y - this.selectionStartWorld.y) > 0.1; if (!hasSizeX || !hasSizeY) { this.selectionStartWorld = null; this.selectionEndWorld = null; this.performRender(); return; } // Calculate selection rect in world coordinates const worldRect = getSelectionRect(this.selectionStartWorld.x, this.selectionStartWorld.y, this.selectionEndWorld.x, this.selectionEndWorld.y); this.applySelectedArea(worldRect[0], worldRect[1], worldRect[2], worldRect[3]); // Clear selection this.selectionStartWorld = null; this.selectionEndWorld = null; this.performRender(); }; this.setContext({ canvas: this.getCanvas(), ctx: this.getCanvas().getContext("2d"), }); } /** * Called after initialization and when the layer is reattached. * This is where we set up event subscriptions to ensure they work properly * after the layer is unmounted and reattached. */ afterInit() { // Set up event handlers here instead of in constructor this.onGraphEvent("mousedown", this.handleMouseDown, { capture: true, }); // Call parent afterInit to ensure proper initialization super.afterInit(); } render() { this.resetTransform(); if (!this.hasActiveSelection()) { return; } this.drawSelectionArea(); } hasActiveSelection() { return this.selectionStartWorld !== null && this.selectionEndWorld !== null; } drawSelectionArea() { if (!this.selectionStartWorld || !this.selectionEndWorld) { return; } // Calculate selection rectangle in world coordinates // Layer will automatically apply camera transformation thanks to transformByCameraPosition const x = Math.min(this.selectionStartWorld.x, this.selectionEndWorld.x); const y = Math.min(this.selectionStartWorld.y, this.selectionEndWorld.y); const width = Math.abs(this.selectionEndWorld.x - this.selectionStartWorld.x); const height = Math.abs(this.selectionEndWorld.y - this.selectionStartWorld.y); render(this.context.ctx, (ctx) => { ctx.fillStyle = this.context.colors.selection.background; ctx.strokeStyle = this.context.colors.selection.border; ctx.beginPath(); ctx.lineWidth = Math.round(1 / this.context.graph.cameraService.getCameraScale()); ctx.roundRect(x, y, width, height, Number(this.context.graph.layers.getDPR())); ctx.closePath(); ctx.fill(); ctx.stroke(); }); } applySelectedArea(x, y, w, h) { const selectableEntityTypes = this.context.graph.$graphConstants.value.selectionLayer.SELECTABLE_ENTITY_TYPES; const elements = this.context.graph.getElementsOverRect({ x, y, width: w, height: h }, selectableEntityTypes); this.context.graph.rootStore.selectionService.selectRelatedElements(elements, ESelectionStrategy.REPLACE); } }