diff --git a/src/components/ImageView/Image.js b/src/components/ImageView/Image.js index cbbdfc9ee..01f63098d 100644 --- a/src/components/ImageView/Image.js +++ b/src/components/ImageView/Image.js @@ -6,6 +6,16 @@ import messages from '../../utils/messages'; import { ErrorMessage } from '../ErrorMessage/ErrorMessage'; import './Image.styl'; +/** + * Coordinates in relative mode belong to a data domain consisting of percentages in the range from 0 to 100 + */ +export const RELATIVE_STAGE_WIDTH = 100; + +/** + * Coordinates in relative mode belong to a data domain consisting of percentages in the range from 0 to 100 + */ +export const RELATIVE_STAGE_HEIGHT = 100; + export const Image = observer(forwardRef(({ imageEntity, imageTransform, diff --git a/src/mixins/DrawingTool.js b/src/mixins/DrawingTool.js index 66133a080..79d9116fb 100644 --- a/src/mixins/DrawingTool.js +++ b/src/mixins/DrawingTool.js @@ -4,6 +4,7 @@ import Utils from '../utils'; import throttle from 'lodash.throttle'; import { MIN_SIZE } from '../tools/Base'; import { FF_DEV_3666, FF_DEV_3793, isFF } from '../utils/feature-flags'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const DrawingTool = types .model('DrawingTool', { @@ -56,8 +57,8 @@ const DrawingTool = types get MIN_SIZE() { if (isFF(FF_DEV_3793)) { return { - X: MIN_SIZE.X / self.obj.stageScale / self.obj.stageWidth * 100, - Y: MIN_SIZE.Y / self.obj.stageScale / self.obj.stageHeight * 100, + X: MIN_SIZE.X / self.obj.stageScale / self.obj.stageWidth * RELATIVE_STAGE_WIDTH, + Y: MIN_SIZE.Y / self.obj.stageScale / self.obj.stageHeight * RELATIVE_STAGE_HEIGHT, }; } @@ -231,8 +232,8 @@ const TwoPointsDrawingTool = DrawingTool.named('TwoPointsDrawingTool') if (!shape) return; const isEllipse = shape.type.includes('ellipse'); - const maxStageWidth = isFF(FF_DEV_3793) ? 100 : self.obj.stageWidth; - const maxStageHeight = isFF(FF_DEV_3793) ? 100 : self.obj.stageHeight; + const maxStageWidth = isFF(FF_DEV_3793) ? RELATIVE_STAGE_WIDTH : self.obj.stageWidth; + const maxStageHeight = isFF(FF_DEV_3793) ? RELATIVE_STAGE_HEIGHT : self.obj.stageHeight; let { x1, y1, x2, y2 } = isEllipse ? { x1: shape.startX, @@ -499,16 +500,17 @@ const ThreePointsDrawingTool = DrawingTool.named('ThreePointsDrawingTool') const shape = self.getCurrentArea(); if (!shape) return; - const { stageWidth, stageHeight } = self.obj; + const maxStageWidth = isFF(FF_DEV_3793) ? RELATIVE_STAGE_WIDTH : self.obj.stageWidth; + const maxStageHeight = isFF(FF_DEV_3793) ? RELATIVE_STAGE_HEIGHT : self.obj.stageHeight; let { x1, y1, x2, y2 } = Utils.Image.reverseCoordinates({ x: shape.startX, y: shape.startY }, { x, y }); x1 = Math.max(0, x1); y1 = Math.max(0, y1); - x2 = Math.min(stageWidth, x2); - y2 = Math.min(stageHeight, y2); + x2 = Math.min(maxStageWidth, x2); + y2 = Math.min(maxStageHeight, y2); - shape.setPosition(x1, y1, x2 - x1, y2 - y1, shape.rotation); + shape.setPositionInternal(x1, y1, x2 - x1, y2 - y1, shape.rotation); }, finishDrawing(x, y) { diff --git a/src/mixins/Regions.js b/src/mixins/Regions.js index 3ea8d0bc5..eac9e0cb8 100644 --- a/src/mixins/Regions.js +++ b/src/mixins/Regions.js @@ -3,6 +3,7 @@ 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({ @@ -127,19 +128,19 @@ const RegionsMixin = types // @todo this conversion methods should be removed after removing FF_DEV_3793 convertXToPerc(x) { - return (x * 100) / self.currentImageEntity.stageWidth; + return (x * RELATIVE_STAGE_WIDTH) / self.currentImageEntity.stageWidth; }, convertYToPerc(y) { - return (y * 100) / self.currentImageEntity.stageHeight; + return (y * RELATIVE_STAGE_HEIGHT) / self.currentImageEntity.stageHeight; }, convertHDimensionToPerc(hd) { - return (hd * (self.scaleX || 1) * 100) / self.currentImageEntity.stageWidth; + return (hd * (self.scaleX || 1) * RELATIVE_STAGE_WIDTH) / self.currentImageEntity.stageWidth; }, convertVDimensionToPerc(vd) { - return (vd * (self.scaleY || 1) * 100) / self.currentImageEntity.stageHeight; + return (vd * (self.scaleY || 1) * RELATIVE_STAGE_HEIGHT) / self.currentImageEntity.stageHeight; }, // update region appearence based on it's current states, for diff --git a/src/regions/EllipseRegion.js b/src/regions/EllipseRegion.js index ec4d17106..7575a1704 100644 --- a/src/regions/EllipseRegion.js +++ b/src/regions/EllipseRegion.js @@ -19,6 +19,7 @@ import { FF_DEV_3793, isFF } from '../utils/feature-flags'; import { createDragBoundFunc } from '../utils/image'; import { AliveRegion } from './AliveRegion'; import { EditableRegion } from './EditableRegion'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const EllipseRegionAbsoluteCoordsDEV3793 = types .model({ @@ -65,11 +66,11 @@ const EllipseRegionAbsoluteCoordsDEV3793 = types self.radiusX = radiusX; self.radiusY = radiusY; - self.relativeX = (x / self.parent?.stageWidth) * 100; - self.relativeY = (y / self.parent?.stageHeight) * 100; + self.relativeX = (x / self.parent?.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (y / self.parent?.stageHeight) * RELATIVE_STAGE_HEIGHT; - self.relativeRadiusX = (radiusX / self.parent?.stageWidth) * 100; - self.relativeRadiusY = (radiusY / self.parent?.stageHeight) * 100; + self.relativeRadiusX = (radiusX / self.parent?.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeRadiusY = (radiusY / self.parent?.stageHeight) * RELATIVE_STAGE_HEIGHT; self.rotation = (rotation + 360) % 360; }, @@ -81,15 +82,15 @@ const EllipseRegionAbsoluteCoordsDEV3793 = types self.sh = sh; if (self.coordstype === 'px') { - self.x = (sw * self.relativeX) / 100; - self.y = (sh * self.relativeY) / 100; - self.radiusX = (sw * self.relativeRadiusX) / 100; - self.radiusY = (sh * self.relativeRadiusY) / 100; + self.x = (sw * self.relativeX) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.relativeY) / RELATIVE_STAGE_HEIGHT; + self.radiusX = (sw * self.relativeRadiusX) / RELATIVE_STAGE_WIDTH; + self.radiusY = (sh * self.relativeRadiusY) / RELATIVE_STAGE_HEIGHT; } else if (self.coordstype === 'perc') { - self.x = (sw * self.x) / 100; - self.y = (sh * self.y) / 100; - self.radiusX = (sw * self.radiusX) / 100; - self.radiusY = (sh * self.radiusY) / 100; + self.x = (sw * self.x) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.y) / RELATIVE_STAGE_HEIGHT; + self.radiusX = (sw * self.radiusX) / RELATIVE_STAGE_WIDTH; + self.radiusY = (sh * self.radiusY) / RELATIVE_STAGE_HEIGHT; self.coordstype = 'px'; } }, diff --git a/src/regions/KeyPointRegion.js b/src/regions/KeyPointRegion.js index 0fd4924a2..657c16f51 100644 --- a/src/regions/KeyPointRegion.js +++ b/src/regions/KeyPointRegion.js @@ -17,6 +17,7 @@ import { FF_DEV_3793, isFF } from '../utils/feature-flags'; import { createDragBoundFunc } from '../utils/image'; import { AliveRegion } from './AliveRegion'; import { EditableRegion } from './EditableRegion'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const KeyPointRegionAbsoluteCoordsDEV3793 = types .model({ @@ -38,8 +39,8 @@ const KeyPointRegionAbsoluteCoordsDEV3793 = types const { stageWidth: width, stageHeight: height } = self.parent; if (width && height) { - self.relativeX = (self.x / width) * 100; - self.relativeY = (self.y / height) * 100; + self.relativeX = (self.x / width) * RELATIVE_STAGE_WIDTH; + self.relativeY = (self.y / height) * RELATIVE_STAGE_HEIGHT; } } }, @@ -48,20 +49,20 @@ const KeyPointRegionAbsoluteCoordsDEV3793 = types self.x = x; self.y = y; - self.relativeX = (x / self.parent.stageWidth) * 100; - self.relativeY = (y / self.parent.stageHeight) * 100; + self.relativeX = (x / self.parent.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (y / self.parent.stageHeight) * RELATIVE_STAGE_HEIGHT; }, updateImageSize(wp, hp, sw, sh) { if (self.coordstype === 'px') { - self.x = (sw * self.relativeX) / 100; - self.y = (sh * self.relativeY) / 100; + self.x = (sw * self.relativeX) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.relativeY) / RELATIVE_STAGE_HEIGHT; } if (self.coordstype === 'perc') { - self.x = (sw * self.x) / 100; - self.y = (sh * self.y) / 100; - self.width = (sw * self.width) / 100; + self.x = (sw * self.x) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.y) / RELATIVE_STAGE_HEIGHT; + self.width = (sw * self.width) / RELATIVE_STAGE_WIDTH; self.coordstype = 'px'; } }, diff --git a/src/regions/PolygonPoint.js b/src/regions/PolygonPoint.js index 1a32d01a2..de61078f9 100644 --- a/src/regions/PolygonPoint.js +++ b/src/regions/PolygonPoint.js @@ -6,6 +6,7 @@ import { getParent, getRoot, hasParent, types } from 'mobx-state-tree'; import { guidGenerator } from '../core/Helpers'; import { useRegionStyles } from '../hooks/useRegionColor'; import { FF_DEV_2431, FF_DEV_3793, isFF } from '../utils/feature-flags'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const PolygonPointAbsoluteCoordsDEV3793 = types.model() .volatile(() => ({ @@ -23,8 +24,8 @@ const PolygonPointAbsoluteCoordsDEV3793 = types.model() self.relativeX = self.x; self.relativeY = self.y; } else { - self.relativeX = (self.x / self.stage.stageWidth) * 100; - self.relativeY = (self.y / self.stage.stageHeight) * 100; + self.relativeX = (self.x / self.stage.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (self.y / self.stage.stageHeight) * RELATIVE_STAGE_HEIGHT; } }, movePoint(offsetX, offsetY) { @@ -33,15 +34,15 @@ const PolygonPointAbsoluteCoordsDEV3793 = types.model() self.x = self.x + offsetX; self.y = self.y + offsetY; - self.relativeX = (self.x / self.stage.stageWidth) * 100; - self.relativeY = (self.y / self.stage.stageHeight) * 100; + self.relativeX = (self.x / self.stage.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (self.y / self.stage.stageHeight) * RELATIVE_STAGE_HEIGHT; }, _movePoint(x, y) { self.initX = x; self.initY = y; - self.relativeX = (x / self.stage.stageWidth) * 100; - self.relativeY = (y / self.stage.stageHeight) * 100; + self.relativeX = (x / self.stage.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (y / self.stage.stageHeight) * RELATIVE_STAGE_HEIGHT; self.x = x; self.y = y; diff --git a/src/regions/PolygonRegion.js b/src/regions/PolygonRegion.js index e45228181..5133f9f24 100644 --- a/src/regions/PolygonRegion.js +++ b/src/regions/PolygonRegion.js @@ -21,6 +21,7 @@ import { createDragBoundFunc } from '../utils/image'; import { ImageViewContext } from '../components/ImageView/ImageViewContext'; import { FF_DEV_2432, FF_DEV_3793, isFF } from '../utils/feature-flags'; import { fixMobxObserve } from '../utils/utilities'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const PolygonRegionAbsoluteCoordsDEV3793 = types .model({ @@ -30,8 +31,8 @@ const PolygonRegionAbsoluteCoordsDEV3793 = types updateImageSize(wp, hp, sw, sh) { if (self.coordstype === 'px') { self.points.forEach(p => { - const x = (sw * p.relativeX) / 100; - const y = (sh * p.relativeY) / 100; + const x = (sw * p.relativeX) / RELATIVE_STAGE_WIDTH; + const y = (sh * p.relativeY) / RELATIVE_STAGE_HEIGHT; p._movePoint(x, y); }); @@ -39,8 +40,8 @@ const PolygonRegionAbsoluteCoordsDEV3793 = types if (!self.annotation.sentUserGenerate && self.coordstype === 'perc') { self.points.forEach(p => { - const x = (sw * p.x) / 100; - const y = (sh * p.y) / 100; + const x = (sw * p.x) / RELATIVE_STAGE_WIDTH; + const y = (sh * p.y) / RELATIVE_STAGE_HEIGHT; self.coordstype = 'px'; p._movePoint(x, y); diff --git a/src/regions/RectRegion.js b/src/regions/RectRegion.js index 6a7f368ba..482c28568 100644 --- a/src/regions/RectRegion.js +++ b/src/regions/RectRegion.js @@ -18,6 +18,7 @@ import { createDragBoundFunc } from '../utils/image'; import { AliveRegion } from './AliveRegion'; import { EditableRegion } from './EditableRegion'; import { RegionWrapper } from './RegionWrapper'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../components/ImageView/Image'; const RectRegionAbsoluteCoordsDEV3793 = types .model({ @@ -32,7 +33,7 @@ const RectRegionAbsoluteCoordsDEV3793 = types })) .actions(self => ({ afterCreate() { - switch (self.coordstype) { + switch (self.coordstype) { case 'perc': { self.relativeX = self.x; self.relativeY = self.y; @@ -58,11 +59,11 @@ const RectRegionAbsoluteCoordsDEV3793 = types self.width = width; self.height = height; - self.relativeX = (x / self.parent?.stageWidth) * 100; - self.relativeY = (y / self.parent?.stageHeight) * 100; + self.relativeX = (x / self.parent?.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeY = (y / self.parent?.stageHeight) * RELATIVE_STAGE_HEIGHT; - self.relativeWidth = (width / self.parent?.stageWidth) * 100; - self.relativeHeight = (height / self.parent?.stageHeight) * 100; + self.relativeWidth = (width / self.parent?.stageWidth) * RELATIVE_STAGE_WIDTH; + self.relativeHeight = (height / self.parent?.stageHeight) * RELATIVE_STAGE_HEIGHT; self.rotation = (rotation + 360) % 360; }, @@ -71,18 +72,66 @@ const RectRegionAbsoluteCoordsDEV3793 = types }, updateImageSize(wp, hp, sw, sh) { if (self.coordstype === 'px') { - self.x = (sw * self.relativeX) / 100; - self.y = (sh * self.relativeY) / 100; - self.width = (sw * self.relativeWidth) / 100; - self.height = (sh * self.relativeHeight) / 100; + self.x = (sw * self.relativeX) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.relativeY) / RELATIVE_STAGE_HEIGHT; + self.width = (sw * self.relativeWidth) / RELATIVE_STAGE_WIDTH; + self.height = (sh * self.relativeHeight) / RELATIVE_STAGE_HEIGHT; } else if (self.coordstype === 'perc') { - self.x = (sw * self.x) / 100; - self.y = (sh * self.y) / 100; - self.width = (sw * self.width) / 100; - self.height = (sh * self.height) / 100; + self.x = (sw * self.x) / RELATIVE_STAGE_WIDTH; + self.y = (sh * self.y) / RELATIVE_STAGE_HEIGHT; + self.width = (sw * self.width) / RELATIVE_STAGE_WIDTH; + self.height = (sh * self.height) / RELATIVE_STAGE_HEIGHT; self.coordstype = 'px'; } }, + + draw(x, y, points) { + const oldHeight = self.height; + + if (points.length === 1) { + self.width = self.getDistanceBetweenPoints({ x, y }, self); + self.rotation = self.rotationAtCreation = Math.atan2(y - self.y, x - self.x) * (180 / Math.PI); + } else if (points.length === 2) { + const { y: firstPointY, x: firstPointX } = points[0]; + const { y: secondPointY, x: secondPointX } = points[1]; + + if (self.isAboveTheLine(points[0], points[1], { x, y })) { + self.x = secondPointX; + self.y = secondPointY; + self.rotation = self.rotationAtCreation + 180; + } else { + self.x = firstPointX; + self.y = firstPointY; + self.rotation = self.rotationAtCreation; + } + self.height = self.getHeightOnPerpendicular(points[0], points[1], { x, y }); + } + + self.setPosition(self.x, self.y, self.width, self.height, self.rotation); + + const areaBBoxCoords = self?.bboxCoords; + + if ( + areaBBoxCoords?.left < 0 || + areaBBoxCoords?.top < 0 || + areaBBoxCoords?.right > self.parent.stageWidth || + areaBBoxCoords?.bottom > self.parent.stageHeight + ) { + self.height = oldHeight; + } + }, + getHeightOnPerpendicular(pointA, pointB, cursor) { + const dx1 = pointB.x - pointA.x; + const dy1 = pointB.y - pointA.y; + const dy2 = pointB.y - cursor.y; + const dx2 = dy2 / dx1 * dy1; // dx2 / dy1 = dy2 / dx1 (triangle is rotated) + const dx3 = cursor.x - pointB.x - dx2; + const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); + const d3 = dx3 / d2 * dx2; // dx3 / d2 = d3 / dx2 (triangle is inverted) + const h = d2 + d3; + + return Math.abs(h); + }, })); /** @@ -190,16 +239,12 @@ const Model = types }, getHeightOnPerpendicular(pointA, pointB, cursor) { - const dx1 = pointB.x - pointA.x; - const dy1 = pointB.y - pointA.y; - const dy2 = pointB.y - cursor.y; - const dx2 = dy2 / dx1 * dy1; // dx2 / dy1 = dy2 / dx1 (triangle is rotated) - const dx3 = cursor.x - pointB.x - dx2; - const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); - const d3 = dx3 / d2 * dx2; // dx3 / d2 = d3 / dx2 (triangle is inverted) - const h = d2 + d3; + const dX = pointB.x - pointA.x; + const dY = pointB.y - pointA.y; + const s2 = Math.abs(dY * cursor.x - dX * cursor.y + pointB.x * pointA.y - pointB.y * pointA.x); + const ab = Math.sqrt(dY * dY + dX * dX); - return Math.abs(h); + return s2 / ab; }, isAboveTheLine(a, b, c) { @@ -208,15 +253,26 @@ const Model = types draw(x, y, points) { const oldHeight = self.height; + const canvasX = self.parent.internalToCanvasX(x); + const canvasY = self.parent.internalToCanvasY(y); if (points.length === 1) { - self.width = self.getDistanceBetweenPoints({ x, y }, self); - self.rotation = self.rotationAtCreation = Math.atan2(y - self.y, x - self.x) * (180 / Math.PI); + const canvasWidth = self.getDistanceBetweenPoints({ x: canvasX, y: canvasY }, { + x: self.canvasX, + y: self.canvasY, + }); + + self.width = self.parent.canvasToInternalX(canvasWidth); + self.rotation = self.rotationAtCreation = Math.atan2(canvasY - self.canvasY, canvasX - self.canvasX) * (180 / Math.PI); } else if (points.length === 2) { + const canvasPoints = points.map(({ x, y }) => ({ + x: self.parent.internalToCanvasX(x), + y: self.parent.internalToCanvasY(y), + })); const { y: firstPointY, x: firstPointX } = points[0]; const { y: secondPointY, x: secondPointX } = points[1]; - if (self.isAboveTheLine(points[0], points[1], { x, y })) { + if (self.isAboveTheLine(canvasPoints[0], canvasPoints[1], { x: canvasX, y: canvasY })) { self.x = secondPointX; self.y = secondPointY; self.rotation = self.rotationAtCreation + 180; @@ -225,18 +281,22 @@ const Model = types self.y = firstPointY; self.rotation = self.rotationAtCreation; } - self.height = self.getHeightOnPerpendicular(points[0], points[1], { x, y }); - } + const canvasHeight = self.getHeightOnPerpendicular(canvasPoints[0], canvasPoints[1], { + x: canvasX, + y: canvasY, + }); - self.setPosition(self.x, self.y, self.width, self.height, self.rotation); + self.height = self.parent.canvasToInternalY(canvasHeight); + } + self.setPositionInternal(self.x, self.y, self.width, self.height, self.rotation); const areaBBoxCoords = self?.bboxCoords; if ( areaBBoxCoords?.left < 0 || areaBBoxCoords?.top < 0 || - areaBBoxCoords?.right > self.parent.stageWidth || - areaBBoxCoords?.bottom > self.parent.stageHeight + areaBBoxCoords?.right > RELATIVE_STAGE_WIDTH || + areaBBoxCoords?.bottom > RELATIVE_STAGE_HEIGHT ) { self.height = oldHeight; } @@ -405,7 +465,10 @@ const HtxRectangleView = ({ item }) => { item.notifyDrawingFinished(); }; - eventHandlers.dragBoundFunc = createDragBoundFunc(item, { x: item.x - item.bboxCoords.left, y: item.y - item.bboxCoords.top }); + eventHandlers.dragBoundFunc = createDragBoundFunc(item, { + x: item.x - item.bboxCoords.left, + y: item.y - item.bboxCoords.top, + }); } return ( diff --git a/src/tags/control/Rectangle.js b/src/tags/control/Rectangle.js index 3d1558073..0a9c656c6 100644 --- a/src/tags/control/Rectangle.js +++ b/src/tags/control/Rectangle.js @@ -6,7 +6,7 @@ import { customTypes } from '../../core/CustomTypes'; import { AnnotationMixin } from '../../mixins/AnnotationMixin'; import SeparatedControlMixin from '../../mixins/SeparatedControlMixin'; import { ToolManagerMixin } from '../../mixins/ToolManagerMixin'; -import { FF_DEV_2132, FF_DEV_3793, isFF } from '../../utils/feature-flags'; +import { FF_DEV_2132, FF_DEV_3793, FF_LSDV_4673, isFF } from '../../utils/feature-flags'; /** * The `Rectangle` tag is used to add a rectangle (Bounding Box) to an image without selecting a label. This can be useful when you have only one label to assign to a rectangle. @@ -49,7 +49,9 @@ const Model = types type: 'rectangle', }) .volatile(() => ({ - toolNames: isFF(FF_DEV_2132) && !isFF(FF_DEV_3793) ? ['Rect', 'Rect3Point'] : ['Rect'], + toolNames: isFF(FF_DEV_2132) && (!isFF(FF_DEV_3793) || isFF(FF_LSDV_4673)) + ? ['Rect', 'Rect3Point'] + : ['Rect'], })); const RectangleModel = types.compose('RectangleModel', diff --git a/src/tags/object/Image/Image.js b/src/tags/object/Image/Image.js index 0587afe15..715cccb6a 100644 --- a/src/tags/object/Image/Image.js +++ b/src/tags/object/Image/Image.js @@ -21,6 +21,7 @@ import ObjectBase from '../Base'; import { DrawingRegion } from './DrawingRegion'; import { ImageEntityMixin } from './ImageEntityMixin'; import { ImageSelection } from './ImageSelection'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../../../components/ImageView/Image'; const IMAGE_PRELOAD_COUNT = 3; @@ -1080,19 +1081,19 @@ const CoordsCalculations = types.model() // @todo scale? canvasToInternalX(n) { - return n / self.stageWidth * 100; + return n / self.stageWidth * RELATIVE_STAGE_WIDTH; }, canvasToInternalY(n) { - return n / self.stageHeight * 100; + return n / self.stageHeight * RELATIVE_STAGE_HEIGHT; }, internalToCanvasX(n) { - return n / 100 * self.stageWidth; + return n / RELATIVE_STAGE_WIDTH * self.stageWidth; }, internalToCanvasY(n) { - return n / 100 * self.stageHeight; + return n / RELATIVE_STAGE_HEIGHT * self.stageHeight; }, })); diff --git a/src/tags/object/Image/ImageSelection.js b/src/tags/object/Image/ImageSelection.js index ca7d559d8..34b2316dd 100644 --- a/src/tags/object/Image/ImageSelection.js +++ b/src/tags/object/Image/ImageSelection.js @@ -1,6 +1,7 @@ import { getParent, types } from 'mobx-state-tree'; import { ImageSelectionPoint } from './ImageSelectionPoint'; import { FF_DEV_3793, isFF } from '../../../utils/feature-flags'; +import { RELATIVE_STAGE_HEIGHT, RELATIVE_STAGE_WIDTH } from '../../../components/ImageView/Image'; export const ImageSelection = types.model({ start: types.maybeNull(ImageSelectionPoint), @@ -97,7 +98,7 @@ export const ImageSelection = types.model({ if (self.isActive || !self.obj.selectedRegions.length) return null; const initial = isFF(FF_DEV_3793) - ? { left: 100, top: 100, right: 0, bottom: 0 } + ? { left: RELATIVE_STAGE_WIDTH, top: RELATIVE_STAGE_HEIGHT, right: 0, bottom: 0 } : { left: self.obj.stageWidth, top: self.obj.stageHeight, right: 0, bottom: 0 }; const bbox = self.obj.selectedRegions.reduce((borders, region) => { return region.bboxCoords ? { diff --git a/src/tools/Rect.js b/src/tools/Rect.js index 3baff1bfe..2c03313ce 100644 --- a/src/tools/Rect.js +++ b/src/tools/Rect.js @@ -5,6 +5,7 @@ import ToolMixin from '../mixins/Tool'; import { ThreePointsDrawingTool, TwoPointsDrawingTool } from '../mixins/DrawingTool'; import { AnnotationMixin } from '../mixins/AnnotationMixin'; import { NodeViews } from '../components/Node/Node'; +import { FF_DEV_3793, isFF } from '../utils/feature-flags'; const _BaseNPointTool = types .model('BaseNTool', { @@ -43,8 +44,8 @@ const _BaseNPointTool = types return Super.createRegionOptions({ x, y, - height: 1, - width: 1, + height: isFF(FF_DEV_3793) ? self.obj.canvasToInternalY(1) : 1, + width: isFF(FF_DEV_3793) ? self.obj.canvasToInternalX(1) : 1, }); }, diff --git a/src/utils/feature-flags.ts b/src/utils/feature-flags.ts index 1eaec7177..12ce6200d 100644 --- a/src/utils/feature-flags.ts +++ b/src/utils/feature-flags.ts @@ -206,6 +206,14 @@ export const FF_LSDV_3012 = 'fflag_feat_front_lsdv_3012_syncable_tags_070423_sho */ export const FF_LSDV_4659 = 'fflag_feat_front_lsdv_4659_skipduplicates_060323_short'; +/** + * Fixes Rect3PointTool behaviour in relative coords mode. + * It also fixes disappearing regions in degenerate cases. + * + * @link https://app.launchdarkly.com/default/production/features/fflag_fix_front_lsdv_4673_rect3point_relative_310523_short + */ +export const FF_LSDV_4673 = 'fflag_fix_front_lsdv_4673_rect3point_relative_310523_short'; + /** * Fixes how presigned urls are generated and accessed to remove possibility of CORS errors. */ diff --git a/tests/functional/data/image_segmentation/tools/rect3point.ts b/tests/functional/data/image_segmentation/tools/rect3point.ts new file mode 100644 index 000000000..6c96e7587 --- /dev/null +++ b/tests/functional/data/image_segmentation/tools/rect3point.ts @@ -0,0 +1,10 @@ +export const rect3Config = ` + + + + +`; + +export const simpleImageData = { + image: 'https://htx-misc.s3.amazonaws.com/opensource/label-studio/examples/images/nick-owuor-astro-nic-visuals-wDifg5xc9Z4-unsplash.jpg', +}; \ No newline at end of file diff --git a/tests/functional/feature-flags.ts b/tests/functional/feature-flags.ts index 5231d8aea..213fa138f 100644 --- a/tests/functional/feature-flags.ts +++ b/tests/functional/feature-flags.ts @@ -4,5 +4,6 @@ export const CURRENT_FLAGS = { [FLAGS.FF_DEV_1170]: true, [FLAGS.FF_PROD_309]: true, [FLAGS.FF_LSDV_4992]: true, + [FLAGS.FF_LSDV_4673]: true, }; diff --git a/tests/functional/package.json b/tests/functional/package.json index 9bd539b63..7df995ded 100644 --- a/tests/functional/package.json +++ b/tests/functional/package.json @@ -18,7 +18,7 @@ "cvg:summary": "nyc report --temp-dir=.nyc_output --reporter=text-summary --cwd=. --exclude-after-remap false" }, "dependencies": { - "@heartexlabs/ls-test": "git+ssh://git@github.com/heartexlabs/ls-frontend-test#1bc78f64a4c0217969af8688c6d7fad30fd61754" + "@heartexlabs/ls-test": "git+ssh://git@github.com/heartexlabs/ls-frontend-test#ade2ae5886bd515e428e5719131b1dc6d3a89650" }, "devDependencies": { "ts-loader": "^9.4.2", diff --git a/tests/functional/specs/image_segmentation/tools/rect3point.cy.ts b/tests/functional/specs/image_segmentation/tools/rect3point.cy.ts new file mode 100644 index 000000000..bdd00282f --- /dev/null +++ b/tests/functional/specs/image_segmentation/tools/rect3point.cy.ts @@ -0,0 +1,176 @@ +import { ImageView, LabelStudio } from '@heartexlabs/ls-test/helpers/LSF'; +import { rect3Config, simpleImageData } from '../../../data/image_segmentation/tools/rect3point'; +import { FF_DEV_2132, FF_DEV_3793, FF_LSDV_4673 } from '../../../../../src/utils/feature-flags'; + +describe('Rect3Point tool', () => { + beforeEach(() => { + LabelStudio.addFeatureFlagsOnPageLoad({ + [FF_DEV_3793]: true, + [FF_DEV_2132]: true, + }); + }); + it('Should be able to draw region with rotation 0', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.selectRect3PointToolByHotkey(); + + ImageView.clickAtRelative(.1,.1); + ImageView.clickAtRelative(.2,.1); + ImageView.clickAtRelative(.2,.2); + + LabelStudio.serialize().then(([result]) => { + expect(result.value.x).to.be.closeTo(10, 0.1); + expect(result.value.y).to.be.closeTo(10, 0.1); + expect(result.value.height).to.be.closeTo(10, 0.1); + expect(result.value.width).to.be.closeTo(10, 0.1); + expect(result.value.rotation).to.be.eq(0); + }); + }); + it('Should be able to draw region with rotation 90', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.selectRect3PointToolByHotkey(); + + ImageView.clickAtRelative(.8,.2); + ImageView.clickAtRelative(.8,.8); + ImageView.clickAtRelative(.2,.8); + + + LabelStudio.serialize().then(([result]) => { + expect(result.value.x).to.be.closeTo(80, 0.2); + expect(result.value.y).to.be.closeTo(20, 0.2); + expect(result.value.rotation).to.be.eq(90); + }); + + }); + it('Should be able to draw region with rotation 180', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.selectRect3PointToolByHotkey(); + + ImageView.clickAtRelative(.6,.6); + ImageView.clickAtRelative(.4,.6); + ImageView.clickAtRelative(.4,.4); + + + LabelStudio.serialize().then(([result]) => { + expect(result.value.x).to.be.closeTo(60, 0.1); + expect(result.value.y).to.be.closeTo(60, 0.1); + expect(result.value.height).to.be.closeTo(20, 0.1); + expect(result.value.width).to.be.closeTo(20, 0.1); + expect(result.value.rotation).to.be.eq(180); + }); + + }); + it('Should be able to draw region with rotation 270', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.selectRect3PointToolByHotkey(); + + ImageView.clickAtRelative(.1,.2); + ImageView.clickAtRelative(.1,.1); + ImageView.clickAtRelative(.2,.1); + + + LabelStudio.serialize().then(([result]) => { + expect(result.value.x).to.be.closeTo(10, 0.2); + expect(result.value.y).to.be.closeTo(20, 0.2); + expect(result.value.rotation).to.be.eq(270); + }); + + }); + it('Should be able to draw region with rotation 45+90*k deg', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.selectRect3PointToolByHotkey(); + //45deg + ImageView.clickAt(100,50); + ImageView.clickAt(150,100); + ImageView.clickAt(100,150); + //135deg + ImageView.clickAt(350,100); + ImageView.clickAt(300,150); + ImageView.clickAt(250,100); + //225deg + ImageView.clickAt(100,350); + ImageView.clickAt(50,300); + ImageView.clickAt(100,250); + //315deg + ImageView.clickAt(250,300); + ImageView.clickAt(300,250); + ImageView.clickAt(350,300); + + LabelStudio.serialize().then((result) => { + expect(result[0].value.rotation).to.be.eq(45); + expect(result[1].value.rotation).to.be.eq(135); + expect(result[2].value.rotation).to.be.eq(225); + expect(result[3].value.rotation).to.be.eq(315); + }); + + }); + it('Should display line of zero height (2 points)', () => { + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.capture('canvas'); + + ImageView.selectRect3PointToolByHotkey(); + ImageView.clickAtRelative(.1, .5); + ImageView.clickAtRelative(.8, .5); + + ImageView.canvasShouldChange('canvas', 0); + }); + it('Should draw by dblclick at the target point', () =>{ + LabelStudio.params() + .config(rect3Config) + .data(simpleImageData) + .withResult([]) + .init(); + + ImageView.waitForImage(); + + ImageView.clickAtRelative(.5, .5); + ImageView.clickAtRelative(.5, .5); + + LabelStudio.serialize().then(([result]) => { + expect(result.value.x).to.be.closeTo(50, 0.1); + expect(result.value.y).to.be.closeTo(50, 0.1); + expect(result.value.rotation).to.be.eq(0); + }); + }); +}); \ No newline at end of file diff --git a/tests/functional/yarn.lock b/tests/functional/yarn.lock index 16b80926c..fc71cf756 100644 --- a/tests/functional/yarn.lock +++ b/tests/functional/yarn.lock @@ -264,9 +264,9 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@heartexlabs/ls-test@git+ssh://git@github.com/heartexlabs/ls-frontend-test#1bc78f64a4c0217969af8688c6d7fad30fd61754": +"@heartexlabs/ls-test@git+ssh://git@github.com/heartexlabs/ls-frontend-test#88f0a67b4cc7fd198f296522983ffa8451d0761b": version "1.0.8" - resolved "git+ssh://git@github.com/heartexlabs/ls-frontend-test#1bc78f64a4c0217969af8688c6d7fad30fd61754" + resolved "git+ssh://git@github.com/heartexlabs/ls-frontend-test#88f0a67b4cc7fd198f296522983ffa8451d0761b" dependencies: "@cypress/code-coverage" "^3.10.0" "@cypress/webpack-preprocessor" "^5.17.0" @@ -284,9 +284,9 @@ webpack-cli "^5.0.1" yargs "^17.7.1" -"@heartexlabs/ls-test@git+ssh://git@github.com/heartexlabs/ls-frontend-test#4b8f05de9decb0987696492eac3150b08134c596": +"@heartexlabs/ls-test@git+ssh://git@github.com/heartexlabs/ls-frontend-test#ade2ae5886bd515e428e5719131b1dc6d3a89650": version "1.0.8" - resolved "git+ssh://git@github.com/heartexlabs/ls-frontend-test#4b8f05de9decb0987696492eac3150b08134c596" + resolved "git+ssh://git@github.com/heartexlabs/ls-frontend-test#ade2ae5886bd515e428e5719131b1dc6d3a89650" dependencies: "@cypress/code-coverage" "^3.10.0" "@cypress/webpack-preprocessor" "^5.17.0"