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


import { getThemeStore } from '@src/theme';
import { SettingField, SettingsTab, SettingsValues } from '@src/types';

export interface RectangleStyle {
  borderColor: string;
  fillColor: string;
}

export interface RectangleTextStyle {
  text: string;
  fontSize: number;
  isBold: boolean;
  isItalic: boolean;
  textColor: string;
}

export type RectangleSettings = RectangleStyle & RectangleTextStyle & SettingsValues;

export function createDefaultSettings(): RectangleSettings {
  const { colors } = getThemeStore();

  return {
    borderColor: colors.rectangleBorderFill,
    fillColor: colors.rectangleAreaFill,
    text: '',
    fontSize: 14,
    isBold: false,
    isItalic: false,
    textColor: colors.chartPriceLineText,
  };
}

export function getRectangleSettingsTabs(settings: RectangleSettings): SettingsTab[] {
  const styleFields: SettingField[] = [
    {
      key: 'borderColor',
      label: 'Цвет границы',
      type: 'color',
      defaultValue: settings.borderColor,
    },
    {
      key: 'fillColor',
      label: 'Цвет фона',
      type: 'color',
      defaultValue: settings.fillColor,
    },
  ];

  const textFields: SettingField[] = [
    {
      key: 'fontSize',
      label: 'Размер текста',
      type: 'number',
      defaultValue: settings.fontSize,
      min: 8,
      max: 24,
    },
    {
      key: 'text',
      label: 'Текст',
      type: 'textarea',
      defaultValue: settings.text,
      placeholder: 'Введите текст',
    },
    {
      key: 'isBold',
      label: 'Жирный текст',
      type: 'boolean',
      defaultValue: settings.isBold,
    },
    {
      key: 'isItalic',
      label: 'Курсив',
      type: 'boolean',
      defaultValue: settings.isItalic,
    },
    {
      key: 'textColor',
      label: 'Цвет текста',
      type: 'color',
      defaultValue: settings.textColor,
    },
  ];

  return [
    {
      key: 'style',
      label: 'Стиль',
      fields: styleFields,
    },
    {
      key: 'text',
      label: 'Текст',
      fields: textFields,
    },
  ];
}



import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';

import { getThemeStore } from '@src/theme';

import { Rectangle } from './rectangle';

const UI = {
  borderWidth: 1,
  handleSize: 10,
  handleBorderWidth: 1,
  textOffset: 8,
  textLineHeightMultiplier: 1.2,
};

export class RectanglePaneRenderer implements IPrimitivePaneRenderer {
  private readonly rectangle: Rectangle;

  constructor(rectangle: Rectangle) {
    this.rectangle = rectangle;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    const data = this.rectangle.getRenderData();

    if (!data) {
      return;
    }

    const { colors } = getThemeStore();

    target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
      const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);

      const left = data.left * horizontalPixelRatio;
      const right = data.right * horizontalPixelRatio;
      const top = data.top * verticalPixelRatio;
      const bottom = data.bottom * verticalPixelRatio;

      context.save();

      if (data.showFill) {
        context.fillStyle = data.fillColor;
        context.fillRect(left, top, right - left, bottom - top);
      }

      context.lineWidth = UI.borderWidth * pixelRatio;
      context.strokeStyle = data.borderColor;
      context.strokeRect(left, top, right - left, bottom - top);

      drawRectangleText(context, {
        left,
        right,
        top,
        text: data.text,
        fontSize: data.fontSize,
        isBold: data.isBold,
        isItalic: data.isItalic,
        textColor: data.textColor,
        pixelRatio,
      });

      if (data.showHandles) {
        for (const handle of Object.values(data.handles)) {
          drawHandle(
            context,
            handle.x * horizontalPixelRatio,
            handle.y * verticalPixelRatio,
            horizontalPixelRatio,
            verticalPixelRatio,
            colors.chartLineColor,
            colors.chartBackground,
          );
        }
      }

      context.restore();
    });
  }
}

function drawRectangleText(
  context: CanvasRenderingContext2D,
  params: {
    left: number;
    right: number;
    top: number;
    text: string;
    fontSize: number;
    isBold: boolean;
    isItalic: boolean;
    textColor: string;
    pixelRatio: number;
  },
): void {
  const { left, right, top, text, fontSize, isBold, isItalic, textColor, pixelRatio } = params;

  if (!text.trim()) {
    return;
  }

  const lines = text.split('\n');
  const safeFontSize = Math.max(1, fontSize);
  const fontSizePx = safeFontSize * pixelRatio;
  const lineHeight = safeFontSize * UI.textLineHeightMultiplier * pixelRatio;
  const fontWeight = isBold ? '700 ' : '';
  const fontStyle = isItalic ? 'italic ' : '';
  const textOffset = UI.textOffset * pixelRatio;

  const centerX = (left + right) / 2;
  const blockHeight = lines.length * lineHeight;
  const firstLineY = top - textOffset - blockHeight + lineHeight / 2;

  context.save();

  context.font = `${fontStyle}${fontWeight}${fontSizePx}px Inter, sans-serif`;
  context.fillStyle = textColor;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  lines.forEach((line, index) => {
    context.fillText(line, centerX, firstLineY + index * lineHeight);
  });

  context.restore();
}

function drawHandle(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  horizontalPixelRatio: number,
  verticalPixelRatio: number,
  strokeColor: string,
  fillColor: string,
): void {
  const width = UI.handleSize * horizontalPixelRatio;
  const height = UI.handleSize * verticalPixelRatio;
  const left = x - width / 2;
  const top = y - height / 2;

  context.save();
  context.fillStyle = fillColor;
  context.strokeStyle = strokeColor;
  context.lineWidth = UI.handleBorderWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);

  context.beginPath();
  context.rect(left, top, width, height);
  context.fill();
  context.stroke();

  context.restore();
}