Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import { types } from "mobx-state-tree";
import { FF_DEV_3793, FF_ZOOM_OPTIM, isFF } from "../utils/feature-flags";
import Constants from "../core/Constants";
 
export const KonvaRegionMixin = types
  .model({})
  .views((self) => {
    return {
      get bboxCoords() {
        console.warn("KonvaRegionMixin needs to implement bboxCoords getter in regions");
        return null;
      },
      get bboxCoordsCanvas() {
        const bbox = self.bboxCoords;
 
        if (!isFF(FF_DEV_3793)) return bbox;
        if (!self.parent) return null;
 
        return {
          left: self.parent.internalToCanvasX(bbox.left),
          top: self.parent.internalToCanvasY(bbox.top),
          right: self.parent.internalToCanvasX(bbox.right),
          bottom: self.parent.internalToCanvasY(bbox.bottom),
        };
      },
      get inViewPort() {
        if (!isFF(FF_ZOOM_OPTIM)) return true;
        return (
          !!self &&
          !!self.bboxCoordsCanvas &&
          !!self.object &&
          self.bboxCoordsCanvas.right >= self.object.viewPortBBoxCoords.left &&
          self.bboxCoordsCanvas.bottom >= self.object.viewPortBBoxCoords.top &&
          self.bboxCoordsCanvas.left <= self.object.viewPortBBoxCoords.right &&
          self.bboxCoordsCanvas.top <= self.object.viewPortBBoxCoords.bottom
        );
      },
      get control() {
        // that's a little bit tricky, but it seems that having a tools field is necessary for the region-creating control tag and it's might be a clue
        return self.results.find((result) => result.from_name.tools)?.from_name;
      },
      get canRotate() {
        return self.control?.canrotate && self.supportsRotate;
      },
 
      get supportsTransform() {
        if (self.isReadOnly()) return false;
        return this._supportsTransform && !this.hidden;
      },
    };
  })
  .actions((self) => {
    let deferredSelectId = null;
    const Super = {
      deleteRegion: self.deleteRegion,
    };
 
    return {
      updateCursor(isHovered = false) {
        const stage = self.parent?.stageRef;
        if (!stage) return;
        const style = stage.container().style;
 
        if (isHovered) {
          if (self.annotation.isLinkingMode) {
            style.cursor = Constants.LINKING_MODE_CURSOR;
          } else if (self.type !== "brushregion") {
            style.cursor = Constants.POINTER_CURSOR;
          }
          return;
        }
 
        const selectedTool = self.parent?.getToolsManager().findSelectedTool();
        if (!selectedTool || !selectedTool.updateCursor) {
          style.cursor = Constants.DEFAULT_CURSOR;
        } else {
          selectedTool.updateCursor();
        }
      },
 
      checkSizes() {
        const { naturalWidth, naturalHeight, stageWidth: width, stageHeight: height } = self.parent;
 
        if (width > 1 && height > 1) {
          self.updateImageSize?.(width / naturalWidth, height / naturalHeight, width, height);
        }
      },
 
      selectRegion() {
        self.scrollToRegion();
      },
 
      /**
       * Scrolls to region if possible or scrolls to whole image if needed
       */
      scrollToRegion() {
        const zoomedIn = self.object.zoomScale > 1;
        const canvas = self.shapeRef?.parent?.canvas?._canvas;
        let viewport = canvas;
 
        // `.lsf-main-content` is the main scrollable container for LSF
        while (viewport && !viewport.scrollTop && !viewport.className.includes("main-content")) {
          viewport = viewport.parentElement;
        }
        if (!viewport) return;
 
        // minimum percent of region area to consider it visible
        const VISIBLE_AREA = 0.6;
        // infobar is positioned absolutely, covering part of UI
        const INFOBAR_HEIGHT = 36;
 
        const vBBox = viewport.getBoundingClientRect();
        const cBBox = canvas.getBoundingClientRect();
        // bbox inside canvas; for zoomed images calculations are tough,
        // so we use the whole image so it should be visible enough at the end
        const rBBox = zoomedIn ? { top: 0, bottom: cBBox.height } : self.bboxCoordsCanvas;
        const height = rBBox.bottom - rBBox.top;
        // comparing the closest point of region from top or bottom image edge
        // and how deep is this edge hidden behind respective edge of viewport
        const overTop = rBBox.top - (vBBox.top - cBBox.top);
        const overBottom = canvas.clientHeight - rBBox.bottom - (cBBox.bottom - vBBox.bottom) - INFOBAR_HEIGHT;
        // huge images should be scrolled to the closest edge, not to hidden one
        const isHuge = zoomedIn && canvas.clientHeight > viewport.clientHeight;
 
        // huge region or image cut off by viewport edges — do nothing
        if (overTop < 0 && overBottom < 0) return;
 
        if (overTop < 0 && -overTop / height > 1 - VISIBLE_AREA) {
          // if image is still visible enough — don't scroll
          if (zoomedIn && (cBBox.bottom - vBBox.top) / viewport.clientHeight > 1 - VISIBLE_AREA) return;
          viewport.scrollBy({ top: isHuge ? -overBottom : overTop, left: 0, behavior: "smooth" });
        } else if (overBottom < 0 && -overBottom / height > 1 - VISIBLE_AREA) {
          // if image is still visible enough — don't scroll
          if (zoomedIn && (vBBox.bottom - cBBox.top) / viewport.clientHeight > 1 - VISIBLE_AREA) return;
          viewport.scrollBy({ top: isHuge ? overTop : -overBottom, left: 0, behavior: "smooth" });
        }
      },
 
      onClickRegion(e) {
        const annotation = self.annotation;
        const ev = e?.evt || e;
        const additiveMode = ev?.ctrlKey || ev?.metaKey;
 
        if (e) e.cancelBubble = true;
 
        const isDoubleClick = ev.detail === 2;
 
        if (isDoubleClick) {
          self.onDoubleClickRegion();
          return;
        }
 
        const selectAction = () => {
          self._selectArea(additiveMode);
          deferredSelectId = null;
        };
 
        if (!annotation.isReadOnly() && annotation.isLinkingMode) {
          annotation.addLinkedRegion(self);
          annotation.stopLinkingMode();
          annotation.regionStore.unselectAll();
        } else {
          self._selectArea(additiveMode);
        }
      },
      onDoubleClickRegion() {
        self.requestPerRegionFocus();
        // `selectArea` does nothing when there's a selected region already, but it should rerender to make `requestPerRegionFocus` work,
        // so it needs to use `selectAreas` instead. It contains `unselectAll` for this purpose.
        self.annotation.selectAreas([self]);
      },
      deleteRegion() {
        const selectedTool = self.parent?.getToolsManager().findSelectedTool();
        selectedTool?.enable?.();
        Super.deleteRegion();
      },
    };
  });