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


import { clamp } from 'lodash-es';

import { DrawingBase } from '@core/Drawings/DrawingBase';
import {
  clampPointToContainer as clampPointToContainerInElement,
  getAnchorFromPoint,
  getContainerSize as getElementContainerSize,
  getPriceDelta,
  getXCoordinateFromTime,
  getYCoordinateFromPrice,
  shiftTimeByPixels,
} from '@core/Drawings/helpers';

import type { Anchor, Bounds, ContainerSize, Point } from '@core/Drawings/types';
import type { SettingsValues } from '@src/types';

export type TwoPointAnchorKind = 'start' | 'end';

export interface TwoPointGeometry extends Bounds {
  startPoint: Point;
  endPoint: Point;
}

export abstract class TwoPointDrawingBase<
  TSettings extends SettingsValues = SettingsValues,
  THandleId extends string = string,
> extends DrawingBase<TSettings, THandleId> {
  protected startAnchor: Anchor | null = null;
  protected endAnchor: Anchor | null = null;

  protected getAnchor(kind: TwoPointAnchorKind): Anchor | null {
    return kind === 'start' ? this.startAnchor : this.endAnchor;
  }

  protected setAnchor(kind: TwoPointAnchorKind, anchor: Anchor | null): void {
    if (kind === 'start') {
      this.startAnchor = anchor;
      return;
    }

    this.endAnchor = anchor;
  }

  protected setAnchors(startAnchor: Anchor | null, endAnchor: Anchor | null): void {
    this.startAnchor = startAnchor;
    this.endAnchor = endAnchor;
  }

  protected createAnchor(point: Point): Anchor | null {
    return getAnchorFromPoint(this.chart, this.series, point);
  }

  protected getPointFromAnchor(anchor: Anchor | null): Point | null {
    if (!anchor) {
      return null;
    }

    const x = getXCoordinateFromTime(this.chart, anchor.time, this.series);
    const y = getYCoordinateFromPrice(this.series, anchor.price);

    if (x === null || y === null) {
      return null;
    }

    return {
      x: Number(x),
      y: Number(y),
    };
  }

  protected setAnchorFromPoint(kind: TwoPointAnchorKind, point: Point): boolean {
    const anchor = this.createAnchor(point);

    if (!anchor) {
      return false;
    }

    this.setAnchor(kind, anchor);
    return true;
  }

  protected setAnchorsFromPoints(startPoint: Point, endPoint: Point): boolean {
    const startAnchor = this.createAnchor(startPoint);
    const endAnchor = this.createAnchor(endPoint);

    if (!startAnchor || !endAnchor) {
      return false;
    }

    this.setAnchors(startAnchor, endAnchor);
    return true;
  }

  protected getAnchorTimeCoordinate(kind: TwoPointAnchorKind): number | null {
    const anchor = this.getAnchor(kind);

    if (!anchor) {
      return null;
    }

    const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);

    return coordinate === null ? null : Number(coordinate);
  }

  protected getAnchorPriceCoordinate(kind: TwoPointAnchorKind): number | null {
    const anchor = this.getAnchor(kind);

    if (!anchor) {
      return null;
    }

    const coordinate = getYCoordinateFromPrice(this.series, anchor.price);

    return coordinate === null ? null : Number(coordinate);
  }

  protected moveAnchorsByPixels(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
    dragStartPoint: Point | null,
    point: Point,
  ): boolean {
    if (!dragStartPoint) {
      return false;
    }

    const offsetX = point.x - dragStartPoint.x;
    const priceOffset = getPriceDelta(this.series, dragStartPoint.y, point.y);

    return this.moveAnchors(startAnchor, endAnchor, offsetX, priceOffset);
  }

  protected moveAnchorsWithinContainer(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
    dragStartPoint: Point | null,
    point: Point,
    bounds: Bounds,
  ): boolean {
    if (!dragStartPoint) {
      return false;
    }

    const { width, height } = this.getContainerSize();
    const rawOffsetX = point.x - dragStartPoint.x;
    const rawOffsetY = point.y - dragStartPoint.y;

    const offsetX = clamp(rawOffsetX, -bounds.left, width - bounds.right);
    const offsetY = clamp(rawOffsetY, -bounds.top, height - bounds.bottom);
    const priceOffset = getPriceDelta(this.series, dragStartPoint.y, dragStartPoint.y + offsetY);

    return this.moveAnchors(startAnchor, endAnchor, offsetX, priceOffset);
  }

  protected getContainerSize(): ContainerSize {
    return getElementContainerSize(this.container);
  }

  protected clampPointToContainer(point: Point): Point {
    return clampPointToContainerInElement(point, this.container);
  }

  protected getTwoPointGeometry(): TwoPointGeometry | null {
    const startPoint = this.getPointFromAnchor(this.startAnchor);
    const endPoint = this.getPointFromAnchor(this.endAnchor);

    if (!startPoint || !endPoint) {
      return null;
    }

    return {
      startPoint,
      endPoint,
      left: Math.min(startPoint.x, endPoint.x),
      right: Math.max(startPoint.x, endPoint.x),
      top: Math.min(startPoint.y, endPoint.y),
      bottom: Math.max(startPoint.y, endPoint.y),
    };
  }

  private moveAnchors(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
    offsetX: number,
    priceOffset: number,
  ): boolean {
    if (!startAnchor || !endAnchor) {
      return false;
    }

    const nextStartTime = shiftTimeByPixels(this.chart, startAnchor.time, offsetX, this.series);
    const nextEndTime = shiftTimeByPixels(this.chart, endAnchor.time, offsetX, this.series);

    if (nextStartTime === null || nextEndTime === null) {
      return false;
    }

    this.startAnchor = {
      time: nextStartTime,
      price: startAnchor.price + priceOffset,
    };

    this.endAnchor = {
      time: nextEndTime,
      price: endAnchor.price + priceOffset,
    };

    return true;
  }
}

















import {
  IPrimitivePaneView,
  PrimitiveHoveredItem,
  Time,
  UTCTimestamp,
} from 'lightweight-charts';

import {
  CustomPriceAxisPaneView,
  CustomPriceAxisView,
  CustomTimeAxisPaneView,
  CustomTimeAxisView,
} from '@core/Drawings/axis';
import { isPointInBounds, normalizeBounds } from '@core/Drawings/helpers';
import { TwoPointDrawingBase } from '@core/Drawings/TwoPointDrawingBase';
import { updateViews } from '@core/Drawings/utils';

import { Defaults } from '@src/types/defaults';
import { formatPrice } from '@src/utils';
import { formatDate } from '@src/utils/formatter';

import { RectanglePaneView } from './paneView';
import {
  createDefaultSettings,
  getRectangleSettingsTabs,
  RectangleSettings,
  RectangleStyle,
  RectangleTextStyle,
} from './settings';

import type { DrawingHandle } from '@core/Drawings/handles';
import type { Anchor, AxisLabel, AxisSegment, Bounds, Point } from '@core/Drawings/types';
import type { BaseDrawingParams, ISeriesDrawing } from '@core/Drawings/DrawingBase';
import type { ChartOptionsModel, SettingsTab } from '@src/types';

type RectangleMode = 'idle' | 'drawing' | 'ready' | 'dragging';
type RectangleHandle = 'body' | 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | null;
type RectangleHandleKey = Exclude<RectangleHandle, 'body' | null>;
type TimeLabelKind = 'left' | 'right';
type PriceLabelKind = 'top' | 'bottom';

type RectangleParams = BaseDrawingParams;

interface RectangleState {
  hidden: boolean;
  mode: RectangleMode;
  startTime: Time | null;
  endTime: Time | null;
  startPrice: number | null;
  endPrice: number | null;
  settings: RectangleSettings;
}

interface RectangleDragSnapshot {
  startAnchor: Anchor | null;
  endAnchor: Anchor | null;
}

interface RectangleGeometry extends Bounds {
  width: number;
  height: number;
}

export type RectangleRenderData = RectangleGeometry & RectangleStyle & RectangleTextStyle;

const BODY_HIT_TOLERANCE = 6;
const MIN_RECTANGLE_SIZE = 6;

export class Rectangle
  extends TwoPointDrawingBase<RectangleSettings, RectangleHandleKey>
  implements ISeriesDrawing
{
  private removeSelf?: () => void;
  private openSettings?: () => void;

  protected settings: RectangleSettings = createDefaultSettings();
  protected mode: RectangleMode = 'idle';

  private activeDragTarget: RectangleHandle = null;
  private dragPointerId: number | null = null;
  private dragStartPoint: Point | null = null;
  private dragAnchorSnapshot: RectangleDragSnapshot | null = null;
  private dragGeometrySnapshot: RectangleGeometry | null = null;

  private displayFormat: ChartOptionsModel = {
    dateFormat: Defaults.dateFormat,
    timeFormat: Defaults.timeFormat,
    showTime: Defaults.showTime,
  };

  private readonly paneView: RectanglePaneView;
  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
  private readonly leftTimeAxisView: CustomTimeAxisView;
  private readonly rightTimeAxisView: CustomTimeAxisView;
  private readonly topPriceAxisView: CustomPriceAxisView;
  private readonly bottomPriceAxisView: CustomPriceAxisView;

  constructor({
    chart,
    series,
    container,
    interaction,
    formatObservable,
    removeSelf,
    openSettings,
    initialEvent,
  }: RectangleParams) {
    super({ chart, series, container, interaction });

    this.removeSelf = removeSelf;
    this.openSettings = openSettings;

    this.paneView = new RectanglePaneView(this);
    this.timeAxisPaneView = this.createTimeAxisPaneView();
    this.priceAxisPaneView = this.createPriceAxisPaneView();

    this.leftTimeAxisView = this.createTimeAxisView('left');
    this.rightTimeAxisView = this.createTimeAxisView('right');
    this.topPriceAxisView = this.createPriceAxisView('top');
    this.bottomPriceAxisView = this.createPriceAxisView('bottom');

    this.subscribeFormat(formatObservable, (format) => {
      this.displayFormat = format;
    });

    this.series.attachPrimitive(this);

    if (initialEvent?.sourceEvent) {
      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
    }
  }

  public isCreationPending(): boolean {
    return this.mode === 'idle' || this.mode === 'drawing';
  }

  public getState(): RectangleState {
    return {
      hidden: this.hidden,
      mode: this.mode,
      startTime: this.startAnchor?.time ?? null,
      endTime: this.endAnchor?.time ?? null,
      startPrice: this.startAnchor?.price ?? null,
      endPrice: this.endAnchor?.price ?? null,
      settings: { ...this.settings },
    };
  }

  public setState(state: unknown): void {
    if (!state || typeof state !== 'object') {
      return;
    }

    const nextState = state as Partial<RectangleState>;

    if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
      this.hidden = nextState.hidden;
    }

    if ('mode' in nextState && nextState.mode) {
      this.mode = nextState.mode;
    }

    this.restoreStartAnchor(nextState);
    this.restoreEndAnchor(nextState);

    if ('settings' in nextState && nextState.settings) {
      this.settings = {
        ...createDefaultSettings(),
        ...nextState.settings,
      };
    }

    this.render();
  }

  public getSettingsTabs(): SettingsTab[] {
    return getRectangleSettingsTabs(this.settings);
  }

  public updateAllViews(): void {
    updateViews([
      this.paneView,
      this.timeAxisPaneView,
      this.priceAxisPaneView,
      this.leftTimeAxisView,
      this.rightTimeAxisView,
      this.topPriceAxisView,
      this.bottomPriceAxisView,
    ]);
  }

  public paneViews(): readonly IPrimitivePaneView[] {
    return [this.paneView];
  }

  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
    return [this.timeAxisPaneView];
  }

  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
    return [this.priceAxisPaneView];
  }

  public timeAxisViews() {
    return [this.leftTimeAxisView, this.rightTimeAxisView];
  }

  public priceAxisViews() {
    return [this.topPriceAxisView, this.bottomPriceAxisView];
  }

  public getRenderData(): RectangleRenderData | null {
    if (this.hidden) {
      return null;
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return null;
    }

    return {
      ...geometry,
      ...this.settings,
    };
  }

  protected getDrawingHandles(): readonly DrawingHandle<RectangleHandleKey>[] {
    const geometry = this.getGeometry();

    if (!geometry) {
      return [];
    }

    const { left, right, top, bottom } = geometry;
    const centerX = (left + right) / 2;
    const centerY = (top + bottom) / 2;

    return [
      { id: 'w', x: left, y: centerY, shape: 'rounded' },
      { id: 'sw', x: left, y: bottom, shape: 'circle' },
      { id: 's', x: centerX, y: bottom, shape: 'rounded' },
      { id: 'se', x: right, y: bottom, shape: 'circle' },
      { id: 'e', x: right, y: centerY, shape: 'rounded' },
      { id: 'ne', x: right, y: top, shape: 'circle' },
      { id: 'n', x: centerX, y: top, shape: 'rounded' },
      { id: 'nw', x: left, y: top, shape: 'circle' },
    ];
  }

  protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
    if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
      return null;
    }

    const point = { x, y };

    if (!this.isSelected()) {
      if (!this.containsPoint(point)) {
        return null;
      }

      return {
        cursorStyle: 'pointer',
        externalId: 'rectangle-position',
        zOrder: 'top',
      };
    }

    const handleTarget = this.getDrawingHandleAtPoint(point)?.id;

    if (handleTarget) {
      return {
        cursorStyle: this.getCursorStyle(handleTarget),
        externalId: 'rectangle-position',
        zOrder: 'top',
      };
    }

    if (!this.containsPoint(point)) {
      return null;
    }

    return {
      cursorStyle: 'grab',
      externalId: 'rectangle-position',
      zOrder: 'top',
    };
  }

  protected getTimeAxisSegments(): AxisSegment[] {
    if (!this.shouldShowInteractiveAxis()) {
      return [];
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return [];
    }

    return [this.createAxisSegment(geometry.left, geometry.right)];
  }

  protected getPriceAxisSegments(): AxisSegment[] {
    if (!this.shouldShowInteractiveAxis()) {
      return [];
    }

    const geometry = this.getGeometry();

    if (!geometry) {
      return [];
    }

    return [this.createAxisSegment(geometry.top, geometry.bottom)];
  }

  protected getTimeAxisLabel(kind: string): AxisLabel | null {
    if (!this.shouldShowInteractiveAxis() || (kind !== 'left' && kind !== 'right')) {
      return null;
    }

    const labelKind = kind as TimeLabelKind;

    return this.createAxisLabel(
      this.getTimeCoordinate(labelKind),
      this.getTimeText(labelKind),
    );
  }

  protected getPriceAxisLabel(kind: string): AxisLabel | null {
    if (!this.shouldShowInteractiveAxis() || (kind !== 'top' && kind !== 'bottom')) {
      return null;
    }

    const labelKind = kind as PriceLabelKind;

    return this.createAxisLabel(
      this.getPriceCoordinate(labelKind),
      this.getPriceText(labelKind),
    );
  }

  protected getGeometry(): RectangleGeometry | null {
    const geometry = this.getTwoPointGeometry();

    if (!geometry) {
      return null;
    }

    return {
      left: geometry.left,
      right: geometry.right,
      top: geometry.top,
      bottom: geometry.bottom,
      width: geometry.right - geometry.left,
      height: geometry.bottom - geometry.top,
    };
  }

  protected handleDoubleClick = (event: MouseEvent): void => {
    if (this.hidden || this.mode !== 'ready') {
      return;
    }

    const point = this.getEventPoint(event as PointerEvent);

    if (!this.containsPoint(point) && !this.getDrawingHandleAtPoint(point)) {
      return;
    }

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

    this.openSettings?.();
  };

  protected 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;
    }

    if (!this.isSelected()) {
      if (!this.containsPoint(point)) {
        return;
      }

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

      this.select();
      return;
    }

    const dragTarget = this.getDragTarget(point);

    if (!dragTarget) {
      this.deselect();
      return;
    }

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

    this.startDragging(point, event.pointerId, dragTarget);
  };

  protected handlePointerMove = (event: PointerEvent): void => {
    const point = this.getEventPoint(event);

    if (this.mode === 'drawing') {
      this.updateDrawing(point);
      return;
    }

    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
      return;
    }

    event.preventDefault();

    if (this.activeDragTarget === 'body') {
      this.moveWhole(point);
      this.render();
      return;
    }

    this.resizeRectangle(point);
    this.render();
  };

  protected handlePointerUp = (event: PointerEvent): void => {
    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
      return;
    }

    this.finishDragging();
  };

  private startDrawing(point: Point): void {
    const anchor = this.createAnchor(point);

    if (!anchor) {
      return;
    }

    this.setAnchors(anchor, anchor);
    this.mode = 'drawing';
    this.render();
  }

  private updateDrawing(point: Point): void {
    const clampedPoint = this.clampPointToContainer(point);

    if (!this.setAnchorFromPoint('end', clampedPoint)) {
      return;
    }

    this.render();
  }

  private finishDrawing(): void {
    const geometry = this.getGeometry();

    if (!geometry || geometry.width < MIN_RECTANGLE_SIZE || geometry.height < MIN_RECTANGLE_SIZE) {
      if (this.removeSelf) {
        this.removeSelf();
        return;
      }

      this.resetToIdle();
      return;
    }

    this.mode = 'ready';
    this.resolveReady?.();
    this.render();
  }

  private startDragging(
    point: Point,
    pointerId: number,
    dragTarget: Exclude<RectangleHandle, null>,
  ): void {
    this.mode = 'dragging';
    this.activeDragTarget = dragTarget;
    this.dragPointerId = pointerId;
    this.dragStartPoint = point;
    this.dragAnchorSnapshot = {
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
    };
    this.dragGeometrySnapshot = this.getGeometry();

    this.render();
  }

  private finishDragging(): void {
    this.mode = 'ready';
    this.resolveReady?.();

    this.clearInteractionState();
    this.render();
  }

  private clearInteractionState(): void {
    this.activeDragTarget = null;
    this.dragPointerId = null;
    this.dragStartPoint = null;
    this.dragAnchorSnapshot = null;
    this.dragGeometrySnapshot = null;
  }

  private resetToIdle(): void {
    this.hidden = false;
    this.mode = 'idle';
    this.setAnchors(null, null);

    this.clearInteractionState();
    this.render();
  }

  private getDragTarget(point: Point): Exclude<RectangleHandle, null> | null {
    const handleTarget = this.getDrawingHandleAtPoint(point)?.id;

    if (handleTarget) {
      return handleTarget;
    }

    return this.containsPoint(point) ? 'body' : null;
  }

  private moveWhole(point: Point): void {
    const snapshot = this.dragAnchorSnapshot;
    const geometry = this.dragGeometrySnapshot;

    if (!snapshot || !geometry) {
      return;
    }

    this.moveAnchorsWithinContainer(
      snapshot.startAnchor,
      snapshot.endAnchor,
      this.dragStartPoint,
      point,
      geometry,
    );
  }

  private resizeRectangle(point: Point): void {
    const geometry = this.dragGeometrySnapshot;

    if (!geometry || !this.activeDragTarget || this.activeDragTarget === 'body') {
      return;
    }

    const clampedPoint = this.clampPointToContainer(point);
    let { left, right, top, bottom } = geometry;

    switch (this.activeDragTarget) {
      case 'nw':
        left = clampedPoint.x;
        top = clampedPoint.y;
        break;
      case 'n':
        top = clampedPoint.y;
        break;
      case 'ne':
        right = clampedPoint.x;
        top = clampedPoint.y;
        break;
      case 'e':
        right = clampedPoint.x;
        break;
      case 'se':
        right = clampedPoint.x;
        bottom = clampedPoint.y;
        break;
      case 's':
        bottom = clampedPoint.y;
        break;
      case 'sw':
        left = clampedPoint.x;
        bottom = clampedPoint.y;
        break;
      case 'w':
        left = clampedPoint.x;
        break;
      default:
        return;
    }

    this.setRectangleBounds(left, right, top, bottom);
  }

  private setRectangleBounds(left: number, right: number, top: number, bottom: number): boolean {
    const bounds = normalizeBounds(left, right, top, bottom, this.container);

    return this.setAnchorsFromPoints(
      { x: bounds.left, y: bounds.top },
      { x: bounds.right, y: bounds.bottom },
    );
  }

  private getTimeCoordinate(kind: TimeLabelKind): number | null {
    const geometry = this.getGeometry();

    if (!geometry) {
      return null;
    }

    return kind === 'left' ? geometry.left : geometry.right;
  }

  private getPriceCoordinate(kind: PriceLabelKind): number | null {
    const geometry = this.getGeometry();

    if (!geometry) {
      return null;
    }

    return kind === 'top' ? geometry.top : geometry.bottom;
  }

  private getTimeText(kind: TimeLabelKind): string {
    const time = this.getTimeValueForLabel(kind);

    if (typeof time !== 'number') {
      return '';
    }

    return formatDate(
      time as UTCTimestamp,
      this.displayFormat.dateFormat,
      this.displayFormat.timeFormat,
      this.displayFormat.showTime,
    );
  }

  private getPriceText(kind: PriceLabelKind): string {
    const price = this.getPriceValueForLabel(kind);

    return price === null ? '' : formatPrice(price) ?? '';
  }

  private getTimeValueForLabel(kind: TimeLabelKind): Time | null {
    if (!this.startAnchor || !this.endAnchor) {
      return null;
    }

    const geometry = this.getTwoPointGeometry();

    if (!geometry) {
      return kind === 'left' ? this.startAnchor.time : this.endAnchor.time;
    }

    const startIsLeft = geometry.startPoint.x <= geometry.endPoint.x;

    if (kind === 'left') {
      return startIsLeft ? this.startAnchor.time : this.endAnchor.time;
    }

    return startIsLeft ? this.endAnchor.time : this.startAnchor.time;
  }

  private getPriceValueForLabel(kind: PriceLabelKind): number | null {
    if (!this.startAnchor || !this.endAnchor) {
      return null;
    }

    const geometry = this.getTwoPointGeometry();

    if (!geometry) {
      return kind === 'top'
        ? Math.max(this.startAnchor.price, this.endAnchor.price)
        : Math.min(this.startAnchor.price, this.endAnchor.price);
    }

    const startIsTop = geometry.startPoint.y <= geometry.endPoint.y;

    if (kind === 'top') {
      return startIsTop ? this.startAnchor.price : this.endAnchor.price;
    }

    return startIsTop ? this.endAnchor.price : this.startAnchor.price;
  }

  private containsPoint(point: Point): boolean {
    const geometry = this.getGeometry();

    return geometry !== null && isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
  }

  private getCursorStyle(handle: Exclude<RectangleHandle, null>): PrimitiveHoveredItem['cursorStyle'] {
    switch (handle) {
      case 'nw':
      case 'se':
        return 'nwse-resize';
      case 'ne':
      case 'sw':
        return 'nesw-resize';
      case 'n':
      case 's':
        return 'ns-resize';
      case 'e':
      case 'w':
        return 'ew-resize';
      case 'body':
        return 'grab';
      default:
        return 'default';
    }
  }

  private restoreStartAnchor(state: Partial<RectangleState>): void {
    if (!('startTime' in state) && !('startPrice' in state)) {
      return;
    }

    const time = 'startTime' in state ? state.startTime ?? null : this.startAnchor?.time ?? null;
    const price = 'startPrice' in state ? state.startPrice ?? null : this.startAnchor?.price ?? null;

    this.startAnchor = time !== null && price !== null ? { time, price } : null;
  }

  private restoreEndAnchor(state: Partial<RectangleState>): void {
    if (!('endTime' in state) && !('endPrice' in state)) {
      return;
    }

    const time = 'endTime' in state ? state.endTime ?? null : this.endAnchor?.time ?? null;
    const price = 'endPrice' in state ? state.endPrice ?? null : this.endAnchor?.price ?? null;

    this.endAnchor = time !== null && price !== null ? { time, price } : null;
  }
}