у меня есть код
import { useEffect, useRef, useState } from 'react';
import Konva from 'konva';
import { useGridStore, useSelectionStore } from '../store';
import { useDocumentContext } from './useDocumentContext';
import { Subject } from 'rxjs';
import { Primitives } from '../models/types';
import { IPoint } from '../models/infrastucture/point';
import { PrimitiveBase } from '../models/base/primitives.base';
import { useUpdateConnectors } from './useUpdateConnectors';
import Node = Konva.Node;
import Stage = Konva.Stage;
import { TextPrimitive } from '../models/primitives/primitives';
import { EditorPrimitives } from '../models/primitives/enums/editor-primitives';
import { getConnectors, getConnectorsId, getConnectorsLine } from '../utils/getConnectors';
import { useStageInteractionContext } from './useStageInteractionContext';
import { IRect } from 'konva/lib/types';

const positionSource = new Subject<{ id: string | number; x: number; y: number }>();
const orderSource = new Subject<{ pageId: string | number; primitives: Map<string | number, Primitives> }>();
const transformSource = new Subject<{
  id: string | number;
  pageId: string | number;
  scaleX: number;
  scaleY: number;
  rotation: number;
  transformTextSize: boolean;
}>();
const connectorPositionSource = new Subject<Map<string | number, IPoint>>();

export const position$ = positionSource.asObservable();
export const order$ = orderSource.asObservable();
export const transform$ = transformSource.asObservable();
export const connectorPosition$ = connectorPositionSource.asObservable();

interface UseSnapProps {
  primitive: PrimitiveBase;
  isExporting?: boolean;
}

export const useSnap = <T extends Node>({ primitive, isExporting }: UseSnapProps) => {
  const gridSize = useGridStore((state) => state.gridSize);
  const document = useDocumentContext();
  const ref = useRef<T>(null);
  const { getConnectorsPositions } = useUpdateConnectors();
  const [isCtrlPressed, setIsCtrlPressed] = useState<boolean>(false);
  const [initialPosition, setInitialPosition] = useState<IPoint | null>(null);

  const { selectedObjects, setSelectedObjects, setSelectedObjectReferenceId } = useSelectionStore();
  const stageInteraction = useStageInteractionContext();
  const { scale: stageScale, point: stagePoint } = stageInteraction;

  // Обработчики событий для отслеживания нажатия клавиши Ctrl
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Control') {
        setIsCtrlPressed(true);
      }
    };

    const handleKeyUp = (e: KeyboardEvent) => {
      if (e.key === 'Control') {
        setIsCtrlPressed(false);
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);

    // Очистка обработчиков при размонтировании компонента
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, []);

  /**
   * Получает абсолютную позицию коннектора на сцене.
   */
  const getAbsolutePosition = (): IPoint => {
    if (!ref.current) return { x: 0, y: 0 };

    const absolutePos = ref.current.getAbsoluteTransform().point({ x: 0, y: 0 });
    return { x: absolutePos.x, y: absolutePos.y };
  };

  /**
   * Вычисляет параметры необходимые для расчета привязки:
   * nodeWithConnectors - информация о примитиве с учетом коннекторов
   * nodeWithoutConnectors - информация о примитиве без учета коннекторов
   * x, y - точка левого верхнего угла
   * endX, endY - точка правого нижнего угла
   * width, height - ширина и высота
   * @param node - примитив
   */
  const getBoundingBox = (node: Node | Stage) => {
    const fromAbsolute = (value: number, point: number) => {
      return (value - point) / stageScale;
    };

    // Переводит абсолютные значения для прямоугольника
    const boxFromAbsolute = (value: IRect) => {
      return {
        x: fromAbsolute(value.x, stagePoint.x),
        y: fromAbsolute(value.y, stagePoint.y),
        width: value.width / stageScale,
        height: value.height / stageScale,
        endX: fromAbsolute(value.x, stagePoint.x) + value.width / stageScale,
        endY: fromAbsolute(value.y, stagePoint.y) + value.height / stageScale,
      };
    };

    // Получаем все коннекторы примитива
    const connectorsGroup = getConnectors(node);
    const connectorsLine = getConnectorsLine(node);

    // Сохраняем первоначальные значения видимости коннекторов
    const visibles = connectorsGroup.map((connector) => connector.getAttr('visible'));

    // Скрываем все коннекторы и текст для расчета границ
    connectorsGroup.forEach((connector) => {
      connector.visible(false);
    });
    const texts = 'children' in node ? node.getChildren().filter((x) => x.getClassName() === 'Text') : [];
    texts.forEach((text) => {
      text.visible(false);
    });
    const nodeWithoutConnectors = boxFromAbsolute(node.getClientRect({ skipStroke: true }));
    const nodeWithConnectors = boxFromAbsolute(node.getClientRect({ skipStroke: true }));

    // Включаем видимость всех коннекторов и текста для расчета границ
    connectorsGroup.forEach((connector) => {
      connector.visible(true);
    });
    texts.forEach((text) => {
      text.visible(true);
    });

    // Рассчитываем положение рамки объекта, учитывая коннекторы
    connectorsLine.forEach((x) => {
      const lineRect = boxFromAbsolute(x.getClientRect());
      if (lineRect.x < nodeWithConnectors.x) {
        nodeWithConnectors.x = lineRect.x;
      }
      if (lineRect.y < nodeWithConnectors.y) {
        nodeWithConnectors.y = lineRect.y;
      }
      if (lineRect.endX > nodeWithoutConnectors.x + nodeWithoutConnectors.width) {
        nodeWithConnectors.endX = lineRect.endX;
      }
      if (lineRect.endY > nodeWithoutConnectors.y + nodeWithoutConnectors.height) {
        nodeWithConnectors.endY = lineRect.endY;
      }
    });
    nodeWithConnectors.width = nodeWithConnectors.endX - nodeWithConnectors.x;
    nodeWithConnectors.height = nodeWithConnectors.endY - nodeWithConnectors.y;

    // Возвращаем коннекторам первоначальную видимость
    connectorsGroup.forEach((connector, index) => {
      connector.visible(visibles[index]);
    });

    return {
      nodeWithConnectors,
      nodeWithoutConnectors,
    };
  };

  /**
   * Ограничивает позицию объекта в пределах страницы и сетки.
   */
  const constrainPosition = (node: Node | Stage, newPosition: IPoint) => {
    if (!document.activePage) {
      return;
    }

    // Получаем значения отступов
    const offsetX = node.getAttr('offsetX') || 0;
    const offsetY = node.getAttr('offsetY') || 0;

    const boundingBox = getBoundingBox(node);

    // Рассчитываем минимально допустимые значения для верхнего левого угла рамки объекта (без коннекторов)
    const minX = Math.max(
      Math.abs(boundingBox.nodeWithConnectors.x - boundingBox.nodeWithoutConnectors.x) + offsetX,
      0,
    );
    const minY = Math.max(
      Math.abs(boundingBox.nodeWithConnectors.y - boundingBox.nodeWithoutConnectors.y) + offsetY,
      0,
    );

    // Рассчитываем максимальные допустимые значения для верхнего левого угла рамки объекта (без коннекторов)
    const maxX =
      document.activePage.width - (boundingBox.nodeWithConnectors.endX - boundingBox.nodeWithoutConnectors.x);
    const maxY =
      document.activePage.height - (boundingBox.nodeWithConnectors.endY - boundingBox.nodeWithoutConnectors.y);

    // Привязка точки к сетке
    const snapToGrid = (value: number, gridSize: number) => {
      return Math.round(value / gridSize) * gridSize;
    };

    // Максимально допустимое число клеток сетки для позиционирования точки привязки
    const maxGridCellsX = Math.round(maxX / gridSize);
    const maxGridCellsY = Math.round(maxY / gridSize);

    // Привязываем точку левого верхнего угла примитива (без учета коннекторов),
    // ограничивая при этом примитив в рамках листа (с учетом коннекторов)
    const snappedPosition = {
      x: snapToGrid(Math.min(Math.max(boundingBox.nodeWithoutConnectors.x, minX), maxGridCellsX * gridSize), gridSize),
      y: snapToGrid(Math.min(Math.max(boundingBox.nodeWithoutConnectors.y, minY), maxGridCellsY * gridSize), gridSize),
    };

    // Проверяем выход за границы листа поле округления
    if (snappedPosition.x < minX) {
      snappedPosition.x += gridSize;
    }
    if (snappedPosition.y < minY) {
      snappedPosition.y += gridSize;
    }
    if (snappedPosition.x > maxX) {
      snappedPosition.x -= gridSize;
    }
    if (snappedPosition.y > maxY) {
      snappedPosition.y -= gridSize;
    }

    // Смещаем координату примитива на размер сдвига точки привязки
    const position = {
      x: Math.round(newPosition.x + snappedPosition.x - boundingBox.nodeWithoutConnectors.x),
      y: Math.round(newPosition.y + snappedPosition.y - boundingBox.nodeWithoutConnectors.y),
    };

    newPosition.x = position.x;
    newPosition.y = position.y;
  };

  useEffect(() => {
    const node = ref.current;
    if (primitive.inGroup || primitive.type !== EditorPrimitives.Text || !node) {
      return;
    }
    updatePosition(node.x(), node.y());
  }, [(primitive as TextPrimitive).offsetX, (primitive as TextPrimitive).offsetY]);

  /**
   * Обновляет позицию объекта и эмиттит изменения.
   */
  const updatePosition = (x: number, y: number) => {
    if (isExporting) {
      return;
    }
    const node = ref.current;
    if (!node) return;

    const newPosition = { x, y };
    constrainPosition(node, newPosition);

    node.position(newPosition);

    positionSource.next({ id: primitive.id, x: newPosition.x, y: newPosition.y });
  };

  /**
   * Переставляет элемент в конец порядка отображения.
   * entity получает максимальный order, остальные сдвигаются на -1.
   */
  const reorderElements = (entityId: string | number) => {
    if (!document.activePage || document.activePage.primitives.size === 0 || isExporting) return;

    // Клонируем primitives, чтобы избежать мутаций оригинальных данных
    const primitives = new Map(Array.from(document.activePage.primitives, ([key, value]) => [key, { ...value }]));

    const entity = primitives.get(entityId);
    if (!entity) {
      console.error('Entity not found');
      return;
    }

    const currentOrder = entity.order;

    // Найти максимальный order среди всех элементов
    let maxOrder = -1;
    for (const primitive of primitives.values()) {
      if (primitive.order > maxOrder) {
        maxOrder = primitive.order;
      }
    }

    // Если entity уже имеет максимальный order, ничего не делаем
    if (currentOrder === maxOrder) {
      console.log('Entity уже имеет максимальный order');
      return;
    }

    // Сдвигаем order всех элементов с order > currentOrder на -1
    primitives.forEach((primitive) => {
      if (primitive.order > currentOrder) {
        primitive.order -= 1;
        primitives.set(primitive.id, primitive);
      }
    });

    // Устанавливаем entity.order на максимальный +1
    entity.order = maxOrder;
    primitives.set(entityId, entity);

    // Обновляем источник порядка
    orderSource.next({ pageId: document.activePage.id, primitives });
  };

  /**
   * Обработчик события начала перетаскивания.
   */
  const handleDragStart = (e: Konva.KonvaEventObject<DragEvent>) => {
    if (!(e.evt.buttons & 1)) {
      e.target.stopDrag();
      return;
    }

    // Можно добавить логику, связанную с глобальным Transformer
    setInitialPosition(e.target.position());
  };

  /**
   * Обработчик события перемещения при перетаскивании.
   */
  const handleDragMove = (e: Konva.KonvaEventObject<DragEvent>) => {
    const node = e.target;
    const newPosition = {
      x: node.x(),
      y: node.y(),
    };

    constrainPosition(node, newPosition);

    node.position(newPosition);
  };

  /**
   * Обработчик события окончания перетаскивания.
   */
  const handleDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
    const node = e.target;
    node.moveToTop();
    reorderElements(node.id());

    if (!initialPosition || initialPosition === node.position()) {
      return;
    }
    updatePosition(node.x(), node.y());

    const positions = getConnectorsPositions(node);
    connectorPositionSource.next(positions);

    setInitialPosition(null);
  };

  const handleClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
    e.evt.preventDefault();
    e.currentTarget.stopDrag();

    if (primitive.inGroup && !e.currentTarget) {
      return;
    }

    // Только левая и правая кнопки мыши
    // Так же исключаем случай, когда правая кнопка нажимается на несколько выбранных объектов без зажатого ctrl
    if (
      (e.evt.button !== 0 && e.evt.button !== 2) ||
      (e.evt.button === 2 && !isCtrlPressed && selectedObjects?.length > 1)
    ) {
      return;
    }

    const targetId = e.target.id();

    if (targetId.includes('connector')) {
      console.log('connector clicked hoc');
      return;
    }

    if (
      'objectReferenceId' in primitive &&
      typeof primitive.objectReferenceId === 'string' &&
      'functionId' in primitive &&
      primitive.functionId !== primitive.objectReferenceId
    ) {
      setSelectedObjectReferenceId(primitive.objectReferenceId);
    } else {
      setSelectedObjectReferenceId(null);
    }

    let id = e.currentTarget.id().toString();
    if (id.includes('frame-')) {
      id = id.replace('frame-', '');
    } else if (e.target.parent?.id().toString().startsWith('connection-')) {
      id = e.target.parent
        .id()
        .toString()
        .replace(/connection-(.+?)-segment-\d+/, '$1');
    }

    const chosenObject = {
      id: id,
      points: { x: e.currentTarget.x(), y: e.currentTarget.y() },
    };

    if (e.evt.ctrlKey || e.evt.metaKey) {
      const isSelected = selectedObjects.map((object) => object.id)?.includes(chosenObject.id);
      if (isSelected) {
        setSelectedObjects(selectedObjects.filter((x) => x.id !== chosenObject.id));
      } else {
        setSelectedObjects([...selectedObjects, chosenObject]);
      }
    } else {
      setSelectedObjects([chosenObject]);
    }
  };

  const handleTransformEnd = (evt: Konva.KonvaEventObject<Event>) => {
    if (!ref.current || isExporting) {
      return;
    }

    const scaleX = ref.current.scaleX();
    const scaleY = ref.current.scaleY();
    const rotation = ref.current.rotation();

    updatePosition(ref.current.x(), ref.current.y());
    transformSource.next({
      id: primitive.id,
      pageId: document.activePage?.id ?? '0',
      scaleX: scaleX,
      scaleY: scaleY,
      rotation: rotation,
      transformTextSize: isCtrlPressed,
    });

    const positions = getConnectorsPositions(evt.target);
    connectorPositionSource.next(positions);
  };

  return { ref, handleDragStart, handleDragMove, handleDragEnd, handleClick, handleTransformEnd, getAbsolutePosition };
};

export default useSnap;

и задача
Доработать механизм перемещения графического элемента на листе графического документа: перемещать графический элемент только в случае, если он выделен
Open
 Issue
created
2 weeks ago
by
Дмитрий Чуланов
Обоснование необходимости
Текущее поведение: графический элемент перемещается при зажатии левой кнопки мыши и перемещении указателя мыши даже в том случае, если графический элемент не выделен.

Постановка задачи
Реализовать возможность перемещения графического элемента в графическом документе только в том случае, если графический элемент выделен.

Ожидаемое поведение:

При нажатии или зажатии (нажатие без отпускания) левой кнопки мыши над графическим элементом графический элемент выделяется (появляется рамка).
При перемещении указателя мыши с зажатой над графическим элементом левой кнопкой мыши осуществляется перемещение выделенного графического элемента с привязкой к сетке.

сделай