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


diff --git a/CHANGELOG.md b/CHANGELOG.md
index 838190944d0d8495df324d5bd5008040e8da00a1..e0db5328422a945504ca18a7a3f86aa97b0fc04a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # latest
 
 - Добавлена возможность работы с дровингами на второстепенных пейнах
+- Исправлено взаимодействие с пересекающимися дровингами
 
 # 0.1.19
 
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index 47a873bf138401e441898e5dc2667d494cd0f600..58e6e168238b230355e66ca2841c580ccfdfffcc 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -141,7 +141,7 @@ export class Drawing extends DOMObject {
     return this.drawingName;
   }
 
-  public getLwcDrawing(): ISeriesDrawing {
+  public getSeriesDrawing(): ISeriesDrawing {
     return this.lwcDrawing;
   }
 
diff --git a/src/core/Drawings/SeriesDrawingBase.ts b/src/core/Drawings/SeriesDrawingBase.ts
index 00ab6e6c2f29c7f8f5498d4b243b1384e97a9607..2cc853902ff83a615f8dcc0053e5da402e30f2e6 100644
--- a/src/core/Drawings/SeriesDrawingBase.ts
+++ b/src/core/Drawings/SeriesDrawingBase.ts
@@ -52,6 +52,13 @@ export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
 
   subscribeSettings(callback: (settings: SettingsValues) => void): Subscription;
 
+  isHit(event: MouseEvent): boolean;
+  pointerDown(event: PointerEvent): void;
+  pointerMove(event: PointerEvent): void;
+  pointerUp(event: PointerEvent): void;
+  doubleClick(event: MouseEvent): void;
+  contextMenu(event: MouseEvent): void;
+
   getRenderData(): unknown;
 }
 
@@ -204,6 +211,64 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
     };
   }
 
+  public isHit(event: MouseEvent): boolean {
+    if (!this.isEventInside(event)) {
+      return false;
+    }
+
+    const point = this.getEventPoint(event as PointerEvent);
+
+    return this.getHoveredItem(point.x, point.y) !== null;
+  }
+
+  public pointerDown(event: PointerEvent): void {
+    if (!this.isEventInside(event)) {
+      return;
+    }
+
+    if (!this.isLocked() || this.isCreationPending() || event.button !== 0) {
+      this.handlePointerDown(event);
+
+      return;
+    }
+
+    const point = this.getEventPoint(event);
+
+    if (this.getHoveredItem(point.x, point.y)) {
+      this.select();
+
+      return;
+    }
+
+    if (this.isSelected()) {
+      this.deselect();
+    }
+  }
+
+  public pointerMove(event: PointerEvent): void {
+    this.handlePointerMove(event);
+  }
+
+  public pointerUp(event: PointerEvent): void {
+    this.handlePointerUp(event);
+  }
+
+  public doubleClick(event: MouseEvent): void {
+    if (!this.isEventInside(event)) {
+      return;
+    }
+
+    this.handleDoubleClick(event);
+  }
+
+  public contextMenu(event: MouseEvent): void {
+    if (!this.isEventInside(event)) {
+      return;
+    }
+
+    this.handleContextMenu(event);
+  }
+
   public abstract getRenderData(): unknown; // todo: make proper type
   public abstract getState(): unknown;
   public abstract getSettingsTabs(): SettingsTab[];
@@ -267,14 +332,6 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
     }
 
     this.isBound = true;
-
-    this.container.addEventListener('dblclick', this.handleDoubleClick);
-    this.container.addEventListener('pointerdown', this.handlePointerDownEvent);
-    this.container.addEventListener('contextmenu', this.handleContextMenu);
-
-    window.addEventListener('pointermove', this.handlePointerMove);
-    window.addEventListener('pointerup', this.handlePointerUp);
-    window.addEventListener('pointercancel', this.handlePointerUp);
   }
 
   protected unbindEvents(): void {
@@ -283,14 +340,6 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
     }
 
     this.isBound = false;
-
-    this.container.removeEventListener('dblclick', this.handleDoubleClick);
-    this.container.removeEventListener('pointerdown', this.handlePointerDownEvent);
-    this.container.removeEventListener('contextmenu', this.handleContextMenu);
-
-    window.removeEventListener('pointermove', this.handlePointerMove);
-    window.removeEventListener('pointerup', this.handlePointerUp);
-    window.removeEventListener('pointercancel', this.handlePointerUp);
   }
 
   // todo: хочется общую реализацию для каждой кнопки
@@ -331,24 +380,7 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
     );
   }
 
-  protected handlePointerDownEvent = (event: PointerEvent): void => {
-    if (!this.isLocked() || this.isCreationPending() || event.button !== 0) {
-      this.handlePointerDown(event);
-
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-    const isDrawingHit = this.getHoveredItem(point.x, point.y) !== null;
-
-    if (isDrawingHit) {
-      this.select();
-
-      return;
-    }
-
-    if (this.isSelected()) {
-      this.deselect();
-    }
-  };
+  private isEventInside(event: MouseEvent): boolean {
+    return event.target instanceof Node && this.container.contains(event.target);
+  }
 }
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 653e50a3dde04df5206e2f0550aa7c71e14f8b53..d6e9ee1117ca30f8e8713e251bdb900bb86df3c1 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -66,6 +66,7 @@ export class DrawingsManager {
   private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null); // todo: переместить в DrawingsManagerCollection
   private pendingSnapshot: DrawingsManagerSnapshot | null = null;
   private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
+  private interactionDrawing: Drawing | null = null;
   private recreateScheduled = false;
   private pane: Pane;
 
@@ -117,10 +118,13 @@ export class DrawingsManager {
       }),
     );
 
+    window.addEventListener('pointermove', this.handlePointerMove);
     window.addEventListener('pointerup', this.handlePointerUp);
     window.addEventListener('pointercancel', this.handlePointerUp);
     this.container.addEventListener('click', this.handleClick);
     this.container.addEventListener('pointerdown', this.handlePointerDown);
+    this.container.addEventListener('dblclick', this.handleDoubleClick);
+    this.container.addEventListener('contextmenu', this.handleContextMenu);
     // todo: implement ctrl+v
     // hotkeys.register({
     //   keys: [Keys.control, Keys.v],
@@ -147,49 +151,129 @@ export class DrawingsManager {
     // })
   }
 
-  private handlePointerDown = (): void => {
+  private handlePointerDown = (event: PointerEvent): void => {
     this.selectedDrawingSnapshot = null;
 
-    queueMicrotask(() => {
-      const drawing = this.selectedDrawing$.value;
+    if (event.button === 0) {
+      const drawings = this.drawings$.value;
+      const selectedDrawing = this.selectedDrawing$.value;
+      const pendingDrawing =
+        this.interactionDrawing?.isCreationPending() === true
+          ? this.interactionDrawing
+          : drawings.find((drawing) => drawing.isCreationPending());
 
-      if (!drawing || drawing.isCreationPending()) {
-        return;
-      }
+      const drawing =
+        pendingDrawing ??
+        (selectedDrawing?.getSeriesDrawing().isHit(event) ? selectedDrawing : null) ??
+        this.findTopDrawing(event);
 
-      this.selectedDrawingSnapshot = this.createDrawingSnapshot(drawing);
-    });
+      if (drawing) {
+        this.interactionDrawing = drawing;
+
+        if (!drawing.isCreationPending()) {
+          this.selectedDrawingSnapshot = this.createDrawingSnapshot(drawing);
+        }
+
+        drawing.getSeriesDrawing().pointerDown(event);
+      } else {
+        selectedDrawing?.getSeriesDrawing().pointerDown(event);
+      }
+    }
 
     this.DOM.refreshEntities();
   };
 
-  private handlePointerUp = (): void => {
+  private handlePointerMove = (event: PointerEvent): void => {
+    this.interactionDrawing?.getSeriesDrawing().pointerMove(event);
+  };
+
+  private handlePointerUp = (event: PointerEvent): void => {
     const previousSnapshot = this.selectedDrawingSnapshot;
+    const drawing = this.interactionDrawing;
+
+    drawing?.getSeriesDrawing().pointerUp(event);
 
     this.selectedDrawingSnapshot = null;
 
-    queueMicrotask(() => {
-      if (!previousSnapshot) {
-        return;
+    if (previousSnapshot) {
+      const currentDrawing = this.findDrawing(previousSnapshot.id);
+
+      if (currentDrawing && !currentDrawing.isCreationPending()) {
+        this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(currentDrawing));
       }
+    }
 
-      const drawing = this.findDrawing(previousSnapshot.id);
+    if (drawing && !drawing.isCreationPending()) {
+      this.interactionDrawing = null;
+    }
 
-      if (!drawing || drawing.isCreationPending()) {
-        return;
-      }
+    this.DOM.refreshEntities();
+    this.updateActiveTool();
+  };
 
-      this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
-    });
+  private handleDoubleClick = (event: MouseEvent): void => {
+    const drawings = this.drawings$.value;
+    const pendingDrawing =
+      this.interactionDrawing?.isCreationPending() === true
+        ? this.interactionDrawing
+        : drawings.find((drawing) => drawing.isCreationPending());
+
+    const drawing = pendingDrawing ?? this.findTopDrawing(event);
+
+    if (!drawing) {
+      return;
+    }
+
+    if (!drawing.isCreationPending() && drawing !== this.selectedDrawing$.value) {
+      this.selectedDrawing$.next(drawing);
+    }
+
+    drawing.getSeriesDrawing().doubleClick(event);
+
+    if (this.interactionDrawing && !this.interactionDrawing.isCreationPending()) {
+      this.interactionDrawing = null;
+    }
 
     this.DOM.refreshEntities();
     this.updateActiveTool();
   };
 
+  private handleContextMenu = (event: MouseEvent): void => {
+    const drawings = this.drawings$.value;
+    const pendingDrawing =
+      this.interactionDrawing?.isCreationPending() === true
+        ? this.interactionDrawing
+        : drawings.find((drawing) => drawing.isCreationPending());
+
+    const drawing = pendingDrawing ?? this.findTopDrawing(event);
+
+    drawing?.getSeriesDrawing().contextMenu(event);
+
+    if (this.interactionDrawing && !this.interactionDrawing.isCreationPending()) {
+      this.interactionDrawing = null;
+    }
+  };
+
   private handleClick = (): void => {
     this.DOM.refreshEntities();
   };
 
+  private findTopDrawing(event: MouseEvent): Drawing | null {
+    let topDrawing: Drawing | null = null;
+
+    for (const drawing of this.drawings$.value) {
+      if (!drawing.getSeriesDrawing().isHit(event)) {
+        continue;
+      }
+
+      if (!topDrawing || drawing.zIndex > topDrawing.zIndex) {
+        topDrawing = drawing;
+      }
+    }
+
+    return topDrawing;
+  }
+
   private findDrawing(id: string): Drawing | undefined {
     return this.drawings$.value.find((drawing) => drawing.id === id);
   }
@@ -387,6 +471,10 @@ export class DrawingsManager {
       this.selectedDrawing$.next(null);
     }
 
+    if (this.interactionDrawing && drawingsToRemove.includes(this.interactionDrawing)) {
+      this.interactionDrawing = null;
+    }
+
     drawingsToRemove.forEach((drawing) => {
       drawing.destroy();
       this.DOM.removeEntity(drawing);
@@ -447,7 +535,6 @@ export class DrawingsManager {
     }
 
     const { id, state, isLocked = false, zIndex, shouldUpdateDrawingsList = true } = options;
-
     const shouldSelectAfterCreation = state === undefined;
 
     if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
@@ -576,6 +663,7 @@ export class DrawingsManager {
     }
 
     this.selectedDrawingSnapshot = null;
+    this.interactionDrawing = null;
     this.removeDrawings(this.drawings$.value, false);
 
     const restoredDrawings = snapshot.reduce<Drawing[]>((drawings, item) => {
@@ -607,6 +695,7 @@ export class DrawingsManager {
 
   public activateCrosshair(): void {
     this.removePendingDrawings(false);
+    this.interactionDrawing = null;
     this.setActiveTool('crosshair');
     this.DOM.refreshEntities();
   }
@@ -706,19 +795,24 @@ export class DrawingsManager {
       this.selectedDrawing$.next(null);
     }
 
+    this.interactionDrawing = null;
     this.drawings$.value.forEach((drawing) => drawing.hide());
     this.DOM.refreshEntities();
   }
 
   public destroy(): void {
+    window.removeEventListener('pointermove', this.handlePointerMove);
     window.removeEventListener('pointerup', this.handlePointerUp);
     window.removeEventListener('pointercancel', this.handlePointerUp);
     this.container.removeEventListener('click', this.handleClick);
     this.container.removeEventListener('pointerdown', this.handlePointerDown);
+    this.container.removeEventListener('dblclick', this.handleDoubleClick);
+    this.container.removeEventListener('contextmenu', this.handleContextMenu);
 
     this.drawings$.value.forEach((drawing) => drawing.destroy());
 
     this.selectedDrawingSnapshot = null;
+    this.interactionDrawing = null;
     this.copyPasteBuffer = null;
 
     this.subscriptions.unsubscribe();
diff --git a/src/utils/parseTimeframe.ts b/src/utils/parseTimeframe.ts
index 38c2fa444cbf5c98f3e8757d4c5458464b1fb0f5..4517200a7d8a803a81cf3bbab236fe083b5dc6b6 100644
--- a/src/utils/parseTimeframe.ts
+++ b/src/utils/parseTimeframe.ts
@@ -20,10 +20,16 @@ export function parseTimeframe(timeframe: Timeframes, date?: number) {
 }
 
 export function getStartTime(timeframe: Timeframes, date: number) {
-  const { startOfUnit } = parseTimeframe(timeframe, date);
+  const { candleWidth, startOfUnit } = parseTimeframe(timeframe, date);
+
+  if (candleWidth === 1) {
+    return startOfUnit.unix();
+  }
 
   const sec = timeframeToSeconds(timeframe);
-  return startOfUnit.unix() - (startOfUnit.unix() % sec);
+  const offsetSec = startOfUnit.utcOffset() * 60;
+
+  return startOfUnit.unix() - ((startOfUnit.unix() + offsetSec) % sec);
 }
 
 export const toDayjs: Record<string, ManipulateType> = {