Загрузка данных


private handlePointerDown = (event: PointerEvent): void => {
  if (this.hidden || event.button !== 0) {
    return;
  }

  const point = this.getEventPoint(event);

  if (this.mode === 'idle') {
    event.preventDefault();
    event.stopPropagation();

    this.startDrawing(point);

    return;
  }

  if (this.mode === 'drawing') {
    event.preventDefault();
    event.stopPropagation();

    this.updateDrawing(point);
    this.finishDrawing();

    return;
  }

  if (this.mode !== 'ready') {
    return;
  }

  const pointTarget = this.getPointTarget(point);
  const isNearLine = this.isPointNearLine(point);
  const isDrawingHit = pointTarget !== null || isNearLine;

  if (this.isLocked()) {
    if (isDrawingHit && !this.isActive.value) {
      this.isActive.next(true);
      this.render();
    }

    if (!isDrawingHit && this.isActive.value) {
      this.isActive.next(false);
      this.render();
    }

    return;
  }

  if (!this.isActive.value) {
    if (!isDrawingHit) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    this.isActive.next(true);
    this.render();

    return;
  }

  if (pointTarget === 'start') {
    event.preventDefault();
    event.stopPropagation();

    this.startDragging('dragging-start', point, event.pointerId);

    return;
  }

  if (pointTarget === 'end') {
    event.preventDefault();
    event.stopPropagation();

    this.startDragging('dragging-end', point, event.pointerId);

    return;
  }

  if (isNearLine) {
    event.preventDefault();
    event.stopPropagation();

    this.startDragging('dragging-body', point, event.pointerId);

    return;
  }

  this.isActive.next(false);
  this.render();
};




public getToolbarAnchor(): Point | null {
  const geometry = this.getGeometry();

  if (!geometry) {
    return null;
  }

  return {
    x: (geometry.startPoint.x + geometry.endPoint.x) / 2,
    y: (geometry.startPoint.y + geometry.endPoint.y) / 2,
  };
}


import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import { useEffect, useRef, useState } from 'react';

import { GearIcon, LockIcon, LockOpenIcon, TrashIcon } from '@src/components/Icon';

import styles from './index.module.scss';

import type { Drawing } from '@core/Drawings';
import type { Point } from '@core/Drawings/types';
import type { PointerEvent as ReactPointerEvent, SyntheticEvent } from 'react';
import type { Observable } from 'rxjs';

interface FloatingDrawingToolbarProps {
  selectedDrawing$: Observable<Drawing | null>;
  onToggleLock: () => void;
  onOpenSettings: () => void;
  onDelete: () => void;
}

interface Position {
  x: number;
  y: number;
}

interface DragState {
  pointerId: number;
  startX: number;
  startY: number;
  initialX: number;
  initialY: number;
}

const INITIAL_POSITION: Position = {
  x: 12,
  y: 12,
};

const TOOLBAR_GAP = 8;

export function FloatingDrawingToolbar({
  selectedDrawing$,
  onToggleLock,
  onOpenSettings,
  onDelete,
}: FloatingDrawingToolbarProps) {
  const toolbarRef = useRef<HTMLDivElement | null>(null);
  const dragStateRef = useRef<DragState | null>(null);
  const hasManualPositionRef = useRef(false);

  const [selectedDrawing, setSelectedDrawing] = useState<Drawing | null>(null);
  const [isLocked, setIsLocked] = useState(false);
  const [position, setPosition] = useState<Position>(INITIAL_POSITION);

  useEffect(() => {
    const subscription = selectedDrawing$.subscribe(setSelectedDrawing);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing$]);

  useEffect(() => {
    if (!selectedDrawing) {
      setIsLocked(false);

      return;
    }

    const subscription = selectedDrawing.subscribeIsLocked(setIsLocked);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing]);

  useEffect(() => {
    if (!selectedDrawing || hasManualPositionRef.current) {
      return;
    }

    let frameId: number;

    const updatePosition = (): void => {
      const toolbar = toolbarRef.current;
      const container = toolbar?.parentElement;
      const anchor = selectedDrawing.getToolbarAnchor();

      if (toolbar && container && anchor) {
        const nextPosition = getAutoPosition(anchor, toolbar, container);

        setPosition((currentPosition) => {
          if (currentPosition.x === nextPosition.x && currentPosition.y === nextPosition.y) {
            return currentPosition;
          }

          return nextPosition;
        });
      }

      frameId = requestAnimationFrame(updatePosition);
    };

    frameId = requestAnimationFrame(updatePosition);

    return () => {
      cancelAnimationFrame(frameId);
    };
  }, [selectedDrawing]);

  const clampPosition = (x: number, y: number): Position => {
    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (!toolbar || !container) {
      return { x, y };
    }

    return {
      x: Math.max(0, Math.min(x, container.clientWidth - toolbar.offsetWidth)),
      y: Math.max(0, Math.min(y, container.clientHeight - toolbar.offsetHeight)),
    };
  };

  const handleDragStart = (event: ReactPointerEvent<HTMLButtonElement>): void => {
    if (event.button !== 0) {
      return;
    }

    event.preventDefault();
    event.currentTarget.setPointerCapture(event.pointerId);

    hasManualPositionRef.current = true;

    dragStateRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      initialX: position.x,
      initialY: position.y,
    };
  };

  const handleDrag = (event: ReactPointerEvent<HTMLButtonElement>): void => {
    const dragState = dragStateRef.current;

    if (!dragState || dragState.pointerId !== event.pointerId) {
      return;
    }

    event.preventDefault();

    setPosition(
      clampPosition(
        dragState.initialX + event.clientX - dragState.startX,
        dragState.initialY + event.clientY - dragState.startY,
      ),
    );
  };

  const handleDragEnd = (event: ReactPointerEvent<HTMLButtonElement>): void => {
    if (dragStateRef.current?.pointerId !== event.pointerId) {
      return;
    }

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    dragStateRef.current = null;
  };

  const stopPropagation = (event: SyntheticEvent): void => {
    event.stopPropagation();
  };

  if (!selectedDrawing) {
    return null;
  }

  return (
    <div
      ref={toolbarRef}
      className={styles.toolbar}
      style={{
        transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
      }}
      onClick={stopPropagation}
      onContextMenu={stopPropagation}
      onDoubleClick={stopPropagation}
      onPointerCancel={stopPropagation}
      onPointerDown={stopPropagation}
      onPointerMove={stopPropagation}
      onPointerUp={stopPropagation}
    >
      <button
        type="button"
        className={styles.toolbar_handle}
        onPointerCancel={handleDragEnd}
        onPointerDown={handleDragStart}
        onPointerMove={handleDrag}
        onPointerUp={handleDragEnd}
      >
        <span />
        <span />
        <span />
        <span />
        <span />
        <span />
      </button>

      {selectedDrawing.hasSettings() && (
        <Button
          size="sm"
          className={styles.button}
          onClick={onOpenSettings}
          label={<GearIcon />}
        />
      )}

      <Button
        size="sm"
        className={classNames(styles.button, {
          [styles.pressed]: isLocked,
        })}
        onClick={onToggleLock}
        label={isLocked ? <LockIcon /> : <LockOpenIcon />}
      />

      <Button
        size="sm"
        className={styles.button}
        onClick={onDelete}
        label={<TrashIcon />}
      />
    </div>
  );
}

function getAutoPosition(
  anchor: Point,
  toolbar: HTMLElement,
  container: HTMLElement,
): Position {
  const centeredX = anchor.x - toolbar.offsetWidth / 2;
  const positionAbove = anchor.y - toolbar.offsetHeight - TOOLBAR_GAP;
  const positionBelow = anchor.y + TOOLBAR_GAP;

  const x = Math.max(
    0,
    Math.min(
      Math.round(centeredX),
      container.clientWidth - toolbar.offsetWidth,
    ),
  );

  const y = positionAbove >= 0
    ? positionAbove
    : positionBelow;

  return {
    x,
    y: Math.max(
      0,
      Math.min(
        Math.round(y),
        container.clientHeight - toolbar.offsetHeight,
      ),
    ),
  };
}