UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

252 lines (251 loc) 11.4 kB
import { Layer } from "../../services/Layer"; import { computeCssVariable } from "../../utils/functions"; export class MiniMapLayer extends Layer { constructor(props) { const classNames = Array.isArray(props.classNames) ? props.classNames : []; classNames.push("graph-minimap"); super({ canvas: { zIndex: 300, classNames, transformByCameraPosition: false, }, ...props, }); this.onBlockUpdated = () => { this.calculateViewPortCoords(); this.rerenderMapContent(); }; this.rerenderMapContent = () => { if (!this.context?.ctx) return; this.resetTransform(); this.context.ctx.scale(this.scale, this.scale); this.context.ctx.translate(-this.relativeX, -this.relativeY); this.renderUsableRectBelow(); this.renderBlocks(); this.drawCameraBorderFrame(); }; this.handleMouseDownEvent = (rootEvent) => { rootEvent.stopPropagation(); this.onCameraDrag(rootEvent); this.context.graph.dragService.startDrag({ onUpdate: (event) => this.onCameraDrag(event) }, { stopOnMouseLeave: true }); }; this.minimapWidth = this.props.width || 200; this.minimapHeight = this.props.height || 200; this.cameraBorderSize = this.props.cameraBorderSize || 2; this.cameraBorderColor = this.props.cameraBorderColor || "rgba(255, 119, 0, 0.9)"; this.relativeX = 0; this.relativeY = 0; this.scale = 1; } afterInit() { const minimapPosition = this.getPositionOfMiniMap(this.props.location); const style = document.createElement("style"); style.innerHTML = ` .layer.graph-minimap { top: ${minimapPosition.top}; left: ${minimapPosition.left}; bottom: ${minimapPosition.bottom}; right: ${minimapPosition.right}; width: ${this.props.width || 200}px; height: ${this.props.height || 200}px; border: 2px solid var(--g-color-private-cool-grey-1000-solid); background: lightgrey; }`; this.root.appendChild(style); // Set up event subscriptions here if usableRect is already loaded const usableRect = this.props.graph.api.getUsableRect(); if (!(usableRect.height === 0 && usableRect.width === 0 && usableRect.x === 0 && usableRect.y === 0)) { this.calculateViewPortCoords(); this.rerenderMapContent(); // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted this.onGraphEvent("camera-change", this.rerenderMapContent); this.onGraphEvent("colors-changed", this.rerenderMapContent); this.onGraphEvent("block-change", this.onBlockUpdated); // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup if (this.canvas) { this.onCanvasEvent("mousedown", this.handleMouseDownEvent); } } this.onSignal(this.props.graph.hitTest.$usableRect, () => { this.onBlockUpdated(); this.calculateViewPortCoords(); this.rerenderMapContent(); }); super.afterInit(); } updateCanvasSize() { this.rerenderMapContent(); } calculateViewPortCoords() { const usableRect = this.props.graph.api.getUsableRect(); const xPos = usableRect.x - this.context.constants.system.USABLE_RECT_GAP; const yPos = usableRect.y - this.context.constants.system.USABLE_RECT_GAP; const width = usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2; const height = usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2; if (width > height) { this.scale = this.minimapWidth / width; } else { this.scale = this.minimapHeight / height; } // if minimap not rectangle if (height > this.minimapHeight / this.scale) this.scale = this.minimapHeight / height; if (width > this.minimapWidth / this.scale) this.scale = this.minimapWidth / width; this.relativeX = xPos + width / 2 - this.minimapWidth / this.scale / 2; this.relativeY = yPos + height / 2 - this.minimapHeight / this.scale / 2; } // eslint-disable-next-line complexity drawCameraBorderFrame() { const cameraState = this.props.camera.getCameraState(); const relativeXRight = this.relativeX + this.minimapWidth / this.scale; const relativeYBottom = this.relativeY + this.minimapHeight / this.scale; let width = cameraState.relativeWidth; let height = cameraState.relativeHeight; //camera inverted let xPos = -cameraState.relativeX; let yPos = -cameraState.relativeY; const scaledCameraBorderSize = this.cameraBorderSize / this.scale; if (xPos <= this.relativeX && xPos + width <= this.relativeX) { xPos = this.relativeX; width = scaledCameraBorderSize; } else if (xPos <= this.relativeX && xPos + width > this.relativeX && xPos + width <= relativeXRight) { width = width - (this.relativeX - xPos); xPos = this.relativeX; } else if (xPos <= this.relativeX && xPos + width > relativeXRight) { xPos = this.relativeX; width = this.minimapWidth / this.scale; } else if (xPos >= this.relativeX && xPos < relativeXRight && xPos + width <= relativeXRight) { // do nothing } else if (xPos >= this.relativeX && xPos < relativeXRight && xPos + width > relativeXRight) { width = this.minimapWidth / this.scale - (xPos - this.relativeX); } else if (xPos >= relativeXRight && xPos + width > relativeXRight) { xPos = relativeXRight - scaledCameraBorderSize; width = scaledCameraBorderSize; } if (yPos <= this.relativeY && yPos + height <= this.relativeY) { yPos = this.relativeY; height = scaledCameraBorderSize; } else if (yPos <= this.relativeY && yPos + height > this.relativeY && yPos + height <= relativeYBottom) { height = height - (this.relativeY - yPos); yPos = this.relativeY; } else if (yPos <= this.relativeY && yPos + height > relativeYBottom) { yPos = this.relativeY; height = this.minimapHeight / this.scale; } else if (yPos >= this.relativeY && yPos < relativeYBottom && yPos + height <= relativeYBottom) { // do nothing } else if (yPos >= this.relativeY && yPos < relativeYBottom && yPos + height > relativeYBottom) { height = this.minimapHeight / this.scale - (yPos - this.relativeY); } else if (yPos >= relativeYBottom && yPos + height > relativeYBottom) { yPos = relativeYBottom - scaledCameraBorderSize; height = scaledCameraBorderSize; } this.context.ctx.lineWidth = scaledCameraBorderSize; this.context.ctx.strokeStyle = computeCssVariable(this.cameraBorderColor); this.context.ctx.strokeRect(xPos, yPos, width, height); } getPositionOfMiniMap(location) { let position = { left: "unset", top: "unset", bottom: "unset", right: "unset", }; if (!location || location === "topLeft") { position.top = "0px"; position.left = "0px"; } if (location === "topRight") { position.top = "0px"; position.right = "0px"; } if (location === "bottomRight") { position.bottom = "0px"; position.right = "0px"; } if (location === "bottomLeft") { position.bottom = "0px"; position.left = "0px"; } if (typeof location === "object") { position = location; } return position; } willRender() { if (this.firstRender) { const canvas = this.getCanvas(); const dpr = this.getDRP(); canvas.width = this.minimapWidth * dpr; canvas.height = this.minimapHeight * dpr; canvas.style.width = `${this.minimapWidth}px`; canvas.style.height = `${this.minimapHeight}px`; this.setContext({ canvas, ctx: canvas.getContext("2d"), camera: this.props.camera, constants: this.props.graph.graphConstants, colors: this.props.graph.graphColors, }); } } didRender() { if (this.firstRender) { this.unSubscribeUsableRectLoaded = this.props.graph.hitTest.onUsableRectUpdate((usableRect) => { if (usableRect.height === 0 && usableRect.width === 0 && usableRect.x === 0 && usableRect.y === 0) return; this.calculateViewPortCoords(); this.rerenderMapContent(); // If the layer is already attached, set up event subscriptions here if (this.root) { // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted this.onGraphEvent("camera-change", this.rerenderMapContent); this.onGraphEvent("colors-changed", this.rerenderMapContent); this.onGraphEvent("block-change", this.onBlockUpdated); // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup if (this.canvas) { this.onCanvasEvent("mousedown", this.handleMouseDownEvent); } } this.unSubscribeUsableRectLoaded?.(); }); } } renderUsableRectBelow() { const usableRect = this.props.graph.api.getUsableRect(); this.context.ctx.fillStyle = computeCssVariable(this.context.colors.canvas.layerBackground); const xPos = usableRect.x - this.context.constants.system.USABLE_RECT_GAP; const yPos = usableRect.y - this.context.constants.system.USABLE_RECT_GAP; const width = usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2; const height = usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2; this.context.ctx.fillRect(xPos, yPos, width, height); } renderBlocks() { const blocks = this.props.graph.rootStore.blocksList.$blocks.value; blocks.forEach((block) => { const viewComponent = block.getViewComponent(); viewComponent?.renderMinimalisticBlock(this.context.ctx); }); } onCameraDrag(event) { const cameraState = this.props.camera.getCameraState(); const x = -(this.relativeX + event.offsetX / this.scale) + cameraState.relativeWidth / 2; const y = -(this.relativeY + event.offsetY / this.scale) + cameraState.relativeHeight / 2; const dx = x * cameraState.scale - cameraState.x; const dy = y * cameraState.scale - cameraState.y; this.context.camera.move(dx, dy); } }