import { getEnv, getParent, getRoot, getType, types } from "mobx-state-tree"; import { guidGenerator } from "../core/Helpers"; import { isDefined } from "../utils/utilities"; import { AnnotationMixin } from "./AnnotationMixin"; import { ReadOnlyRegionMixin } from "./ReadOnlyMixin"; import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from "../components/ImageView/Image"; const RegionsMixin = types .model({ // id: types.optional(types.identifier, guidGenerator), pid: types.optional(types.string, guidGenerator), score: types.maybeNull(types.number), filtered: types.optional(types.boolean, false), parentID: types.optional(types.string, ""), fromSuggestion: false, // Dynamic preannotations enabled dynamic: false, origin: types.optional(types.enumeration(["prediction", "prediction-changed", "manual"]), "manual"), item_index: types.maybeNull(types.number), }) .volatile(() => ({ // selected: false, _highlighted: false, hidden: false, locked: false, isDrawing: false, perRegionFocusRequest: null, shapeRef: null, drawingTimeout: null, hideable: true, })) .views((self) => ({ get perRegionStates() { const states = self.states; return states && states.filter((s) => s.perregion === true); }, get store() { return getRoot(self); }, get parent() { return getParent(self); }, get editable() { throw new Error("Not implemented"); }, get isCompleted() { return !self.isDrawing; }, get highlighted() { return self._highlighted; }, get inSelection() { return self.annotation?.regionStore.isSelected(self); }, get isReady() { return true; }, get currentImageEntity() { return self.parent.findImageEntity(self.item_index ?? 0); }, getConnectedDynamicRegions(excludeSelf) { const { regions = [] } = getRoot(self).annotationStore?.selected || {}; const { type, labelName } = self; const result = regions.filter((region) => { if (excludeSelf && region === self) return false; const canBePartOfNotification = self.supportSuggestions ? self.dynamic : true; return ( canBePartOfNotification && region.type === type && region.labelName === labelName && region.results?.[0]?.to_name === self.results?.[0]?.to_name ); }); return result; }, // Indicates that it is not temporary region created just to display data like Textarea's one // and is not a suggestion get isRealRegion() { return self.annotation?.areas?.has(self.id); }, get shouldNotifyDrawingFinished() { // extra calls on destroying will be skipped // @see beforeDestroy action if (!self.isRealRegion) return false; if (self.annotation.isSuggestionsAccepting) return false; // There are two modes: // If object tag support suggestions - the region should be marked as a dynamic one to make notifications // If object tag doesn't support suggestions - every region works as dynamic with auto suggestions const canBeReasonOfNotification = self.supportSuggestions ? self.dynamic && !self.fromSuggestion : true; const isSmartEnabled = self.results.some((r) => r.from_name.smartEnabled); return isSmartEnabled && canBeReasonOfNotification; }, })) .actions((self) => { return { setParentID(id) { self.parentID = id; }, setDrawing(val) { self.isDrawing = val; }, setShapeRef(ref) { if (!ref) return; self.shapeRef = ref; }, setItemIndex(index) { if (!isDefined(index)) throw new Error("Index must be provided for", self); self.item_index = index; }, beforeDestroy() { // beforeDestroy may be called by accident for Textarea and etc. as part of updateObjects action // in that case the region already has no results // The other bad behaviour is that beforeDestroy may be called on accepting suggestions 'cause they are deleting in that case // So if you see this bad thing during debugging - now you know why // and why we need this check if (self.isRealRegion) { return self.beforeDestroyArea(); } }, beforeDestroyArea() { self.notifyDrawingFinished({ destroy: true }); }, setLocked(locked) { if (locked instanceof Function) { self.locked = locked(self.locked); } else { self.locked = locked; } }, makeDynamic() { self.dynamic = true; }, // @todo this conversion methods should be removed after removing FF_DEV_3793 convertXToPerc(x) { return (x * RELATIVE_STAGE_WIDTH) / self.currentImageEntity.stageWidth; }, convertYToPerc(y) { return (y * RELATIVE_STAGE_HEIGHT) / self.currentImageEntity.stageHeight; }, convertHDimensionToPerc(hd) { return (hd * (self.scaleX || 1) * RELATIVE_STAGE_WIDTH) / self.currentImageEntity.stageWidth; }, convertVDimensionToPerc(vd) { return (vd * (self.scaleY || 1) * RELATIVE_STAGE_HEIGHT) / self.currentImageEntity.stageHeight; }, // update region appearence based on it's current states, for // example bbox needs to update its colors when you change the // label, becuase it takes color from the label updateAppearenceFromState() {}, serialize() { console.error("Region class needs to implement serialize"); }, /** @abstract */ selectRegion() {}, /** @abstract */ afterUnselectRegion() {}, onClickRegion(ev) { const annotation = self.annotation; if (!self.isReadOnly() && (self.isDrawing || annotation.isDrawing)) return; if (!self.isReadOnly() && annotation.isLinkingMode) { annotation.addLinkedRegion(self); annotation.stopLinkingMode(); annotation.regionStore.unselectAll(); } else { self._selectArea(ev?.ctrlKey || ev?.metaKey); } }, _selectArea(additiveMode = false) { this.cancelPerRegionFocus(); const annotation = self.annotation; if (additiveMode) { annotation.toggleRegionSelection(self); } else { const wasNotSelected = !self.selected; if (wasNotSelected) { annotation.selectArea(self); } else { annotation.unselectAll(); } } }, requestPerRegionFocus() { self.perRegionFocusRequest = Date.now(); }, cancelPerRegionFocus() { self.perRegionFocusRequest = null; }, setHighlight(val) { self._highlighted = val; }, toggleHighlight() { self.setHighlight(!self._highlighted); }, toggleFiltered(e) { self.filtered = !self.filtered; self.toggleHidden(e, true); e && e.stopPropagation(); }, toggleHidden(e, isFiltered = false) { if (!isFiltered) self.filtered = false; self.hidden = !self.hidden; e && e.stopPropagation(); }, updateOriginOnEdit() { if (self.origin === "prediction") { self.origin = "prediction-changed"; } }, notifyDrawingFinished({ destroy = false } = {}) { self.updateOriginOnEdit(); // everything below is related to dynamic preannotations if (!self.shouldNotifyDrawingFinished) return; clearTimeout(self.drawingTimeout); if (self.isDrawing === false) { const timeout = getType(self).name.match(/brush/i) ? 1200 : 0; const env = getEnv(self); self.drawingTimeout = setTimeout(() => { const connectedRegions = self.getConnectedDynamicRegions(destroy); env.events.invoke("regionFinishedDrawing", self, connectedRegions); }, timeout); } }, }; }); export default types.compose(RegionsMixin, ReadOnlyRegionMixin, AnnotationMixin);