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();
|
},
|
};
|
});
|