const assert = require("assert"); const Asserts = require("../utils/asserts"); const Helpers = require("./helpers"); Feature("Image transformer"); const IMAGE = "https://data.heartex.net/open-images/train_0/mini/0030019819f25b28.jpg"; const annotationEmpty = { id: "1000", result: [], }; const getParamsWithShape = (shape, params = "") => ({ config: ` <${shape} ${params} name="tag" toName="img" /> `, data: { image: IMAGE }, annotations: [annotationEmpty], }); const getParamsWithLabels = (shape) => ({ config: ` <${shape}Labels name="tag" toName="img"> `, data: { image: IMAGE }, annotations: [annotationEmpty], }); const shapes = { Rectangle: { drawAction: "drawByDrag", hasTransformer: true, hasRotator: true, hasMoveToolTransformer: true, hasMultiSelectionTransformer: true, hasMultiSelectionRotator: true, hotKey: "r", byBBox(x, y, width, height) { return { params: [x, y, width, height], result: { width, height, rotation: 0, x, y }, }; }, }, Ellipse: { drawAction: "drawByDrag", hasTransformer: true, hasRotator: true, hasMoveToolTransformer: true, hasMultiSelectionTransformer: true, hasMultiSelectionRotator: true, hotKey: "o", byBBox(x, y, width, height) { return { params: [x + width / 2, y + height / 2, width / 2, height / 2], result: { radiusX: width / 2, radiusY: height / 2, rotation: 0, x: x + width / 2, y: y + height / 2, }, }; }, }, Polygon: { drawAction: "drawByClickingPoints", hasTransformer: false, hasRotator: false, hasMoveToolTransformer: true, hasMultiSelectionTransformer: true, hasMultiSelectionRotator: false, hotKey: "p", byBBox(x, y, width, height) { const points = []; points.push([x, y]); points.push([x + width, y]); points.push([x + width / 2, y + height / 2]); points.push([x + width, y + height]); points.push([x, y + height]); return { params: [[...points, points[0]]], result: { points, closed: true, }, }; }, }, KeyPoint: { drawAction: "clickAt", hasTransformer: false, hasRotator: false, hasMoveToolTransformer: false, hasMultiSelectionTransformer: true, hasMultiSelectionRotator: false, hotKey: "k", params: 'strokeWidth="2"', byBBox(x, y, width, height) { return { params: [x + width / 2, y + height / 2], result: { x: x + width / 2, y: y + height / 2, width: 2, }, }; }, }, }; function drawShapeByBbox(Shape, x, y, width, height, where) { where[Shape.drawAction](...Shape.byBBox(x, y, width, height).params); } const shapesTable = new DataTable(["shapeName"]); for (const shapeName of Object.keys(shapes)) { shapesTable.add([shapeName]); } Data(shapesTable) .Scenario( "Check transformer existing for different shapes, their amount and modes.", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; I.amOnPage("/"); const bbox1 = { x: 100, y: 100, width: 200, height: 200, }; const bbox2 = { x: 400, y: 100, width: 200, height: 200, }; const getCenter = (bbox) => [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]; let isTransformerExist; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); LabelStudio.init(getParamsWithLabels(shapeName)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); // Draw two regions I.pressKey("1"); drawShapeByBbox(Shape, bbox1.x, bbox1.y, bbox1.width, bbox1.height, AtImageView); AtOutliner.seeRegions(1); I.pressKey("1"); drawShapeByBbox(Shape, bbox2.x, bbox2.y, bbox2.width, bbox2.height, AtImageView); AtOutliner.seeRegions(2); // Check that it wasn't a cause to show a transformer isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, false); // Select the first region AtImageView.clickAt(...getCenter(bbox1)); AtOutliner.seeSelectedRegion(); // Match if transformer exist with expectations in single selected mode isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, Shape.hasTransformer); // Match if rotator at transformer exist with expectations in single selected mode isTransformerExist = await AtImageView.isRotaterExist(); assert.strictEqual(isTransformerExist, Shape.hasRotator); // Switch to move tool I.pressKey("v"); // Match if rotator at transformer exist with expectations in single selected mode with move tool chosen isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, Shape.hasMoveToolTransformer); // Deselect the previous selected region I.pressKey(["u"]); // Select 2 regions AtImageView.drawThroughPoints( [ [bbox1.x - 5, bbox1.y - 5], [bbox2.x + bbox2.width + 5, bbox2.y + bbox2.height + 5], ], "steps", 10, ); // Match if transformer exist with expectations in multiple selected mode isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, Shape.hasMultiSelectionTransformer); // Match if rotator exist with expectations in multiple selected mode isTransformerExist = await AtImageView.isRotaterExist(); assert.strictEqual(isTransformerExist, Shape.hasMultiSelectionRotator); }, ) .retry(3); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMoveToolTransformer)).Scenario( "Resizing a single region", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const convertToImageSize = Helpers.getSizeConvertor(canvasSize.width, canvasSize.height); // Draw a region in bbox {x1:50,y1:50,x2:150,y2:150} I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, 50, 50, 100, 100, AtImageView); AtOutliner.seeRegions(1); AtOutliner.dontSeeIncompleteRegion(); // Select the shape AtImageView.clickAt(100, 100); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // Transform the shape // Move the top anchor up for 50px (limited by image border) => {x1:50,y1:0,x2:150,y2:150} AtImageView.drawByDrag(100, 50, 0, -100); // Move the left anchor left for 50px (limited by image border) => {x1:0,y1:0,x2:150,y2:150} AtImageView.drawByDrag(50, 75, -300, -100); // Move the right anchor left for 50px => {x1:0,y1:0,x2:100,y2:150} AtImageView.drawByDrag(150, 75, -50, 0); // Move the bottom anchor down for 100px => {x1:0,y1:0,x2:100,y2:250} AtImageView.drawByDrag(50, 150, 10, 100); // Move the right-bottom anchor right for 200px and down for 50px => {x1:0,y1:0,x2:300,y2:300} AtImageView.drawByDrag(100, 250, 200, 50); // Check resulting sizes const rectangleResult = await LabelStudio.serialize(); const exceptedResult = Shape.byBBox(0, 0, 300, 300).result; Asserts.deepEqualWithTolerance(rectangleResult[0].value, convertToImageSize(exceptedResult)); }, ); // Currently flipping is handled correctly only for rectangles. Data(shapesTable.filter(({ shapeName }) => shapeName === "Rectangle")).Scenario( "Flip region during resizing", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const convertToImageSize = Helpers.getSizeConvertor(canvasSize.width, canvasSize.height); let rectangleResult; // Draw a region in bbox {x1:50,y1:50,x2:150,y2:150} I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, 50, 50, 100, 100, AtImageView); AtOutliner.seeRegions(1); AtOutliner.dontSeeIncompleteRegion(); // Select the shape AtImageView.clickAt(100, 100); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // Flip the shape horizontally // Move the left anchor to the right further than region width, effectively flipping it and reducing width to 50px AtImageView.drawByDrag(50, 100, 150, 0); // Check resulting sizes rectangleResult = await LabelStudio.serialize(); const exceptedResult = Shape.byBBox(150, 50, 50, 100).result; Asserts.deepEqualWithTolerance(rectangleResult[0].value, convertToImageSize(exceptedResult)); // new center of the region const center = [150 + 25, 50 + 50]; // Rotate the shape by 45 degrees, rotation handle is 50px above the top anchor // we move the rotation handle to the right to rotate the shape by 45 degrees AtImageView.drawByDrag(center[0], 0, center[1], 0); rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 45); // Flip the shape horizontally with non-zero rotation const shift = (50 * 2) / Math.SQRT2; AtImageView.drawByDrag(center[0] - 25 / Math.SQRT2, center[1] - 25 / Math.SQRT2, shift, shift); const secondFlipResult = { ...convertToImageSize( Shape.byBBox( center[0] + 25 / Math.SQRT2 + 50 / Math.SQRT2, center[1] + 25 / Math.SQRT2 - 50 / Math.SQRT2, 50, 100, ).result, ), rotation: 45, }; rectangleResult = await LabelStudio.serialize(); // flipping is not very precise, so we have to increase the tolerance Asserts.deepEqualWithTolerance(rectangleResult[0].value, secondFlipResult, 0); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMoveToolTransformer)).Scenario( "Resizing a single region with zoom", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, Regions, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); LabelStudio.setFeatureFlags({ fflag_fix_front_dev_3377_image_regions_shift_on_resize_280922_short: true, fflag_fix_front_dev_3793_relative_coords_short: true, }); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); AtDetailsPanel.seeExpandButton(); await AtImageView.lookForStage(); // Draw a region in bbox {x1:50,y1:50,x2:150,y2:150} I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, 50, 50, 300, 300, AtImageView); AtOutliner.seeRegions(1); AtOutliner.dontSeeIncompleteRegion(); // Select the shape AtImageView.clickAt(100, 100); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // it won't be real zoom scale, so we have to compensate it, // and in the current specific situation it should be done my maxScale const { maxScale } = await AtImageView.getZoomProps(); AtImageView.setZoom(3 * maxScale, 0, 0); await AtImageView.lookForStage(); const prevRegionBBox = await Regions.getBBoxByRegionIdx(0); // Transform the shape AtImageView.drawByDrag(150, 150, -150, -150); AtImageView.drawByDrag(0, 0, -300, -100); AtImageView.drawByDrag(0, 0, 150, 150); // Check resulting sizes const regionBBox = await Regions.getBBoxByRegionIdx(0); Asserts.deepEqualWithTolerance(regionBBox, prevRegionBBox, 2); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Simple rotating", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); // Draw a region in bbox {x1:40%,y1:40%,x2:60%,y2:60%} const rectangle = { x: canvasSize.width * 0.4, y: canvasSize.height * 0.4, width: canvasSize.width * 0.2, height: canvasSize.height * 0.2, }; const rectangleCenter = { x: rectangle.x + rectangle.width / 2, y: rectangle.y + rectangle.height / 2, }; I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, rectangle.x, rectangle.y, rectangle.width, rectangle.height, AtImageView); AtOutliner.seeRegions(1); // Select the shape and check that transformer appears AtImageView.clickAt(rectangleCenter.x, rectangleCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: rectangleCenter.x, y: rectangle.y - 50, }; // Rotate for 45 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [rectangleCenter.x + 500, rectangleCenter.y - 500], ], "steps", 5, ); // Check resulting rotation const rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(Math.round(rectangleResult[0].value.rotation), 45); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Rotating of unrotatable region", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); // Draw a region which we cannot rotate 'cause of position near the image's border {x1:0,y1:20%,x2:20%,y2:50%} const rectangle = { x: 0, y: canvasSize.height * 0.2, width: canvasSize.width * 0.2, height: canvasSize.height * 0.3, }; const rectangleCenter = { x: rectangle.x + rectangle.width / 2, y: rectangle.y + rectangle.height / 2, }; I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, rectangle.x, rectangle.y, rectangle.width, rectangle.height, AtImageView); AtOutliner.seeRegions(1); // Select the shape and check that transformer appears AtImageView.clickAt(rectangleCenter.x, rectangleCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: rectangleCenter.x, y: rectangle.y - 50, }; // Rotate for 45 degrees clockwise AtImageView.drawByDrag(rotatorPosition.x, rotatorPosition.y, rectangleCenter.y - rotatorPosition.y + 100, -100); // Check the region hasn't been rotated const rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 0); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Broke the limits with rotation", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); { // Draw a region which have limitation at rotating by bbox {x1:5,y1:100,x2:305,y2:350} const rectangle = { x: 5, y: 100, width: 300, height: 300, }; const rectangleCenter = { x: rectangle.x + rectangle.width / 2, y: rectangle.y + rectangle.height / 2, }; I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, rectangle.x, rectangle.y, rectangle.width, rectangle.height, AtImageView); AtOutliner.seeRegions(1); // Select the shape and check that transformer appears AtImageView.clickAt(rectangleCenter.x, rectangleCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: rectangleCenter.x, y: rectangle.y - 50, }; // Rotate for 45 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [rectangleCenter.x + 500, rectangleCenter.y - 500], ], "steps", 200, ); // Check that we cannot rotate it like this let rectangleResult = await LabelStudio.serialize(); assert.notStrictEqual(Math.round(rectangleResult[0].value.rotation), 0, "Region must be rotated"); assert.notStrictEqual(Math.round(rectangleResult[0].value.rotation), 45, "Angle must not be 45 degrees"); // Undo changes I.pressKey(["CommandOrControl", "z"]); // Rotate for 90 degrees clockwise instead AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [rectangle.x + rectangle.width + 100, rectangleCenter.y], [rectangle.x + rectangle.width + 200, rectangleCenter.y], ], "steps", 200, ); // Check the resulted rotation rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 90, "Angle must be 90 degrees"); // remove region I.pressKey("Backspace"); } I.say("Check that it works same way with right border"); { // Draw a region which have limitation at rotating by bbox {x1:100% - 305,y1:100,x2:100% - 5,y2:350} const rectangle = { x: canvasSize.width - 305, y: 100, width: 300, height: 300, }; const rectangleCenter = { x: rectangle.x + rectangle.width / 2, y: rectangle.y + rectangle.height / 2, }; I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, rectangle.x, rectangle.y, rectangle.width, rectangle.height, AtImageView); AtOutliner.seeRegions(1); // Select the shape and check that transformer appears AtImageView.clickAt(rectangleCenter.x, rectangleCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: rectangleCenter.x, y: rectangle.y - 50, }; // Rotate for 45 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [rectangleCenter.x + 500, rectangleCenter.y - 500], ], "steps", 200, ); // Check the resulted rotation let rectangleResult = await LabelStudio.serialize(); assert.notStrictEqual(Math.round(rectangleResult[0].value.rotation), 0); assert.notStrictEqual(Math.round(rectangleResult[0].value.rotation), 45); // Undo changes I.pressKey(["CommandOrControl", "z"]); // Rotate for 90 degrees clockwise instead AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [rectangle.x + rectangle.width + 100, rectangleCenter.y], [rectangle.x + rectangle.width + 200, rectangleCenter.y], ], "steps", 200, ); // Check that we cannot rotate it like this rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 90); } }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Check the initial rotation of transformer for the single region", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const bbox = { x: 100, y: 100, width: 100, height: 100, }; const bboxCenter = { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2, }; // Draw a region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox.x, bbox.y, bbox.width, bbox.height, AtImageView); AtOutliner.seeRegions(1); // Select it AtImageView.clickAt(bboxCenter.x, bboxCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels let rotatorPosition = { x: bboxCenter.x, y: bbox.y - 50, }; // Rotate for 90 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [bbox.x + bbox.width + 100, bboxCenter.y], [bbox.x + bbox.width + 200, bboxCenter.y], ], "steps", 10, ); // Unselect current region I.pressKey("u"); AtOutliner.dontSeeSelectedRegion(); // Select it again AtImageView.clickAt(bboxCenter.x, bboxCenter.y); AtOutliner.seeSelectedRegion(); // Wait for transformer to be properly initialized after re-selection I.waitTicks(3); // The trick is that we turn it further, based on the assumption that transformer appears in rotated state on region selection // So let's try to rotate it // The rotator anchor must be to the right of the right anchor by 50 pixels rotatorPosition = { x: bbox.x + bbox.width + 50, y: bboxCenter.y, }; // Rotate for 90 degrees clockwise once again AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [bboxCenter.x, bbox.y + bbox.height + 100], [bboxCenter.x, bbox.y + bbox.height + 200], ], "steps", 10, ); // Check that region has been rotated for 180 degrees const rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 180); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Check the initial rotation of transformer for the couple of regions", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const bbox1 = { x: 100, y: 100, width: 40, height: 40, }; const bbox2 = { x: 160, y: 160, width: 40, height: 40, }; const transformerBbox = { x: bbox1.x, y: bbox1.y, width: bbox2.x + bbox2.width - bbox1.x, height: bbox2.y + bbox2.height - bbox1.y, }; const transformerBboxCenter = { x: transformerBbox.x + transformerBbox.width / 2, y: transformerBbox.y + transformerBbox.height / 2, }; // Draw the first region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox1.x, bbox1.y, bbox1.width, bbox1.height, AtImageView); AtOutliner.seeRegions(1); // Draw the second region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox2.x, bbox2.y, bbox2.width, bbox2.height, AtImageView); AtOutliner.seeRegions(2); // Switch to move tool and select them I.pressKey("v"); AtImageView.drawThroughPoints([ [transformerBbox.x - 20, transformerBbox.y - 20], [transformerBbox.x + transformerBbox.width + 20, transformerBbox.y + transformerBbox.height + 20], ]); AtOutliner.seeSelectedRegion(); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: transformerBboxCenter.x, y: transformerBbox.y - 50, }; // Rotate for 180 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [transformerBboxCenter.x + 100, transformerBboxCenter.y + 100], [transformerBboxCenter.x, transformerBboxCenter.y + 100], [transformerBboxCenter.x, transformerBboxCenter.y + 200], ], "steps", 10, ); // Unselect current regions I.pressKey("u"); AtOutliner.dontSeeSelectedRegion(); // Select them again AtImageView.drawThroughPoints([ [transformerBbox.x - 20, transformerBbox.y - 20], [transformerBbox.x + transformerBbox.width + 20, transformerBbox.y + transformerBbox.height + 20], ]); AtOutliner.seeSelectedRegion(); // Wait for transformer to be properly initialized after re-selection I.waitTicks(3); // So we have couple of rotated regions, let's check if rotates still appears above the top anchor // Rotate for 90 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [transformerBboxCenter.x + 100, transformerBboxCenter.y], [transformerBboxCenter.x + 200, transformerBboxCenter.y], ], "steps", 10, ); // Check that region has been rotated for (180 + 90) degrees const rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 180 + 90); }, ); // KeyPoints are transformed unpredictable so for now just skip them Data( shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionTransformer && shapeName !== "KeyPoint"), ).Scenario( "Transforming of multiple regions", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const convertToImageSize = Helpers.getSizeConvertor(canvasSize.width, canvasSize.height); const bbox1 = { x: 100, y: 100, width: 50, height: 50, }; const bbox2 = { x: 150, y: 150, width: 50, height: 50, }; const transformerBbox = { x: bbox1.x, y: bbox1.y, width: bbox2.x + bbox2.width - bbox1.x, height: bbox2.y + bbox2.height - bbox1.y, }; const transformerBboxCenter = { get x() { return transformerBbox.x + transformerBbox.width / 2; }, get y() { return transformerBbox.y + transformerBbox.height / 2; }, }; // Draw the first region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox1.x, bbox1.y, bbox1.width, bbox1.height, AtImageView); AtOutliner.seeRegions(1); // Draw the second region I.pressKey(Shape.hotKey); I.pressKeyDown("CommandOrControl"); drawShapeByBbox(Shape, bbox2.x, bbox2.y, bbox2.width, bbox2.height, AtImageView); I.pressKeyUp("CommandOrControl"); AtOutliner.seeRegions(2); // Switch to move tool and select them I.pressKey("v"); AtImageView.drawThroughPoints([ [transformerBbox.x - 20, transformerBbox.y - 20], [transformerBbox.x + transformerBbox.width + 20, transformerBbox.y + transformerBbox.height + 20], ]); AtOutliner.seeSelectedRegion(); // Scale the shapes vertically AtImageView.drawByDrag(transformerBboxCenter.x, transformerBbox.y + transformerBbox.height, 0, 50); transformerBbox.height += 50; AtOutliner.seeSelectedRegion(); // Scale the shapes horizontally AtImageView.drawByDrag(transformerBbox.x + transformerBbox.width, transformerBboxCenter.y, 50, 0); transformerBbox.width += 50; AtOutliner.seeSelectedRegion(); // Scale the shapes in both directions AtImageView.drawByDrag( transformerBbox.x + transformerBbox.width, transformerBbox.y + transformerBbox.height, 50, 50, ); transformerBbox.height += 50; transformerBbox.width += 50; AtOutliner.seeSelectedRegion(); // Check resulting sizes const rectangleResult = await LabelStudio.serialize(); const exceptedResult1 = Shape.byBBox(bbox1.x, bbox1.y, bbox1.width + 50, bbox1.height + 50).result; const exceptedResult2 = Shape.byBBox(bbox2.x + 50, bbox2.y + 50, bbox2.width + 50, bbox2.height + 50).result; Asserts.deepEqualWithTolerance(rectangleResult[0].value, convertToImageSize(exceptedResult1)); Asserts.deepEqualWithTolerance(rectangleResult[1].value, convertToImageSize(exceptedResult2)); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionTransformer)).Scenario( "Move regions by drag", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const convertToImageSize = Helpers.getSizeConvertor(canvasSize.width, canvasSize.height); const bbox1 = { x: 100, y: 100, width: 20, height: 20, }; const bbox1Center = { x: bbox1.x + bbox1.width / 2, y: bbox1.y + bbox1.height / 2, }; const bbox2 = { x: 140, y: 140, width: 20, height: 20, }; const bbox2Center = { x: bbox2.x + bbox2.width / 2, y: bbox2.y + bbox2.height / 2, }; const transformerBbox = { x: bbox1.x, y: bbox1.y, width: bbox2.x + bbox2.width - bbox1.x, height: bbox2.y + bbox2.height - bbox1.y, }; const transformerBboxCenter = { x: transformerBbox.x + transformerBbox.width / 2, y: transformerBbox.y + transformerBbox.height / 2, }; // Draw the first region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox1.x, bbox1.y, bbox1.width, bbox1.height, AtImageView); AtOutliner.seeRegions(1); // Draw the second region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox2.x, bbox2.y, bbox2.width, bbox2.height, AtImageView); AtOutliner.seeRegions(2); if (shapeName === "KeyPoint") { // Draw more points to get more space in transformer I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox1.x, bbox1.y, 0, 0, AtImageView); AtOutliner.seeRegions(3); I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox2.x + bbox2.width, bbox2.y + bbox2.height, 0, 0, AtImageView); AtOutliner.seeRegions(4); } // Switch to move tool and select them I.pressKey("v"); AtImageView.drawThroughPoints([ [transformerBbox.x - 20, transformerBbox.y - 20], [transformerBbox.x + transformerBbox.width + 20, transformerBbox.y + transformerBbox.height + 20], ]); AtOutliner.seeSelectedRegion(); const dragShapes = (startPoint, shift, rememberShift = true) => { AtImageView.drawThroughPoints( [ [startPoint.x, startPoint.y], [startPoint.x + shift.x, startPoint.y + shift.y], [startPoint.x + shift.x, startPoint.y + shift.y], ], "steps", 10, ); AtOutliner.seeSelectedRegion(); if (rememberShift) { bbox1Center.x += shift.x; bbox1Center.y += shift.y; bbox2Center.x += shift.x; bbox2Center.y += shift.y; transformerBboxCenter.x += shift.x; transformerBboxCenter.y += shift.y; } }; // Drag shapes by holding onto the first region dragShapes(bbox1Center, { x: 100, y: 0 }); // Drag shapes by holding onto the second region dragShapes(bbox2Center, { x: 0, y: 100 }); // Drag shapes by holding onto the transformer background dragShapes(transformerBboxCenter, { x: 150, y: 150 }, false); // Move back throught history to check that transformer's background moving with it I.pressKey(["CommandOrControl", "z"]); // Drag shapes by holding onto the transformer background again dragShapes(transformerBboxCenter, { x: 100, y: 100 }, false); // Check that dragging was successful const rectangleResult = await LabelStudio.serialize(); const exceptedResult1 = Shape.byBBox(bbox1.x + 200, bbox1.y + 200, bbox1.width, bbox1.height).result; const exceptedResult2 = Shape.byBBox(bbox2.x + 200, bbox2.y + 200, bbox2.width, bbox2.height).result; Asserts.deepEqualWithTolerance(rectangleResult[0].value, convertToImageSize(exceptedResult1)); Asserts.deepEqualWithTolerance(rectangleResult[1].value, convertToImageSize(exceptedResult2)); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Limitation of dragging a single rotated region", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const bbox = { x: canvasSize.width / 2 - 50, y: canvasSize.height / 2 - 50, width: 100, height: 100, }; const bboxCenter = { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2, }; // Draw a region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox.x, bbox.y, bbox.width, bbox.height, AtImageView); AtOutliner.seeRegions(1); // Select it AtImageView.clickAt(bboxCenter.x, bboxCenter.y); AtOutliner.seeSelectedRegion(); // Switch to move tool to force appearance of transformer I.pressKey("v"); const isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, true); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: bboxCenter.x, y: bbox.y - 50, }; // Rotate for 180 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [bboxCenter.x + 100, bboxCenter.y], [bboxCenter.x, bboxCenter.y + 100], [bboxCenter.x, bboxCenter.y + 200], ], "steps", 10, ); // When we have the rotated region, we need to check its behavior when we drag it across the borders of the image let rectangleResult; I.say("Drag the region over the left border"); AtImageView.drawThroughPoints( [ [bboxCenter.x, bboxCenter.y], [-500, bboxCenter.y], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.x * canvasSize.width) / 100, Shape.byBBox(bbox.width, bbox.y + bbox.height, -bbox.width, -bbox.height).result.x, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the top border"); AtImageView.drawThroughPoints( [ [bboxCenter.x, bboxCenter.y], [bboxCenter.x, -500], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.y * canvasSize.height) / 100, Shape.byBBox(bbox.x + bbox.width, bbox.height, -bbox.width, -bbox.height).result.y, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the right border"); AtImageView.drawThroughPoints( [ [bboxCenter.x, bboxCenter.y], [canvasSize.width + 500, bboxCenter.y], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.x * canvasSize.width) / 100, Shape.byBBox(canvasSize.width, bbox.y + bbox.height, -bbox.width, -bbox.height).result.x, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the bottom border"); AtImageView.drawThroughPoints( [ [bboxCenter.x, bboxCenter.y], [bboxCenter.x, canvasSize.height + 500], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.y * canvasSize.height) / 100, Shape.byBBox(bbox.x + bbox.width, canvasSize.height, -bbox.width, -bbox.height).result.y, ); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMultiSelectionRotator)).Scenario( "Limitation of dragging a couple of rotated regions", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const bbox1 = { x: canvasSize.width / 2 - 50, y: canvasSize.height / 2 - 50, width: 50, height: 50, }; const bbox2 = { x: canvasSize.width / 2, y: canvasSize.height / 2, width: 50, height: 50, }; const transformerBbox = { x: bbox1.x, y: bbox1.y, width: bbox2.x + bbox2.width - bbox1.x, height: bbox2.y + bbox2.height - bbox1.y, }; const transformerBboxCenter = { get x() { return transformerBbox.x + transformerBbox.width / 2; }, get y() { return transformerBbox.y + transformerBbox.height / 2; }, }; // Draw the first region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox1.x, bbox1.y, bbox1.width, bbox1.height, AtImageView); AtOutliner.seeRegions(1); // Draw the second region I.pressKey(Shape.hotKey); I.pressKeyDown("CommandOrControl"); drawShapeByBbox(Shape, bbox2.x, bbox2.y, bbox2.width, bbox2.height, AtImageView); I.pressKeyUp("CommandOrControl"); AtOutliner.seeRegions(2); // Select them by move tool I.pressKey("v"); AtImageView.drawThroughPoints( [ [transformerBbox.x - 50, transformerBbox.y - 50], [transformerBbox.x + transformerBbox.width + 50, transformerBbox.y + transformerBbox.height + 50], ], "steps", 10, ); AtOutliner.seeSelectedRegion(); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: transformerBboxCenter.x, y: transformerBbox.y - 50, }; // Rotate for 180 degrees clockwise AtImageView.drawThroughPoints( [ [rotatorPosition.x, rotatorPosition.y], [transformerBboxCenter.x + 100, transformerBboxCenter.y], [transformerBboxCenter.x, transformerBboxCenter.y + 100], [transformerBboxCenter.x, transformerBboxCenter.y + 200], ], "steps", 10, ); // When we have the rotated region, we need to check its behavior when we drag it across the borders of the image let rectangleResult; I.say("Drag the region over the left border"); AtImageView.drawThroughPoints( [ [transformerBboxCenter.x, transformerBboxCenter.y], [-500, transformerBboxCenter.y], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.x * canvasSize.width) / 100, Shape.byBBox(transformerBbox.width, transformerBbox.y + transformerBbox.height, -bbox1.width, -bbox1.height) .result.x, ); Asserts.deepEqualWithTolerance( (rectangleResult[1].value.x * canvasSize.width) / 100, Shape.byBBox(bbox2.width, transformerBbox.y + bbox2.height, -bbox2.width, -bbox2.height).result.x, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the top border"); AtImageView.drawThroughPoints( [ [transformerBboxCenter.x, transformerBboxCenter.y], [transformerBboxCenter.x, -500], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.y * canvasSize.height) / 100, Shape.byBBox(transformerBbox.x + transformerBbox.width, transformerBbox.height, -bbox1.width, -bbox1.height) .result.y, ); Asserts.deepEqualWithTolerance( (rectangleResult[1].value.y * canvasSize.height) / 100, Shape.byBBox(transformerBbox.x + bbox2.width, bbox2.height, -bbox2.width, -bbox2.height).result.y, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the right border"); AtImageView.drawThroughPoints( [ [transformerBboxCenter.x, transformerBboxCenter.y], [canvasSize.width + 500, transformerBboxCenter.y], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.x * canvasSize.width) / 100, Shape.byBBox(canvasSize.width, transformerBbox.y + transformerBbox.height, -bbox1.width, -bbox1.height).result.x, ); Asserts.deepEqualWithTolerance( (rectangleResult[1].value.x * canvasSize.width) / 100, Shape.byBBox( canvasSize.width - transformerBbox.width + bbox2.width, transformerBbox.y + bbox2.height, -bbox2.width, -bbox2.height, ).result.x, ); // reset position by undo I.pressKey(["CommandOrControl", "z"]); I.say("Drag the region over the bottom border"); AtImageView.drawThroughPoints( [ [transformerBboxCenter.x, transformerBboxCenter.y], [transformerBboxCenter.x, canvasSize.height + 500], ], "steps", 20, ); AtOutliner.seeSelectedRegion(); // moving of the region should be constrained by borders rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance( (rectangleResult[0].value.y * canvasSize.height) / 100, Shape.byBBox(transformerBbox.x + transformerBbox.width, canvasSize.height, -bbox1.width, -bbox1.height).result.y, ); Asserts.deepEqualWithTolerance( (rectangleResult[1].value.y * canvasSize.height) / 100, Shape.byBBox( transformerBbox.x + bbox2.width, canvasSize.height - transformerBbox.height + bbox2.height, -bbox2.width, -bbox2.height, ).result.y, ); }, ); Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasRotator)).Scenario( "Rotating the region near the border", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const { shapeName } = current; const Shape = shapes[shapeName]; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(getParamsWithShape(shapeName, Shape.params)); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); await AtImageView.lookForStage(); const canvasSize = await AtImageView.getCanvasSize(); const bbox = { x: canvasSize.width - Math.ceil(Math.sqrt(100 ** 2 + 100 ** 2)) / 2 - 50, y: canvasSize.height - Math.ceil(Math.sqrt(100 ** 2 + 100 ** 2)) / 2 - 50, width: 100, height: 100, }; const bboxCenter = { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2, }; // Draw the region I.pressKey(Shape.hotKey); drawShapeByBbox(Shape, bbox.x, bbox.y, bbox.width, bbox.height, AtImageView); AtOutliner.seeRegions(1); // Select it AtImageView.clickAt(bboxCenter.x, bboxCenter.y); AtOutliner.seeSelectedRegion(); // The rotator anchor must be above top anchor by 50 pixels const rotatorPosition = { x: bboxCenter.x, y: bbox.y - 50, }; // Check 7 different rotations const rotatorWayPoints = [[rotatorPosition.x, rotatorPosition.y]]; const angle45 = Math.PI / 4; for (let i = 0; i < 8; i++) { const angle = angle45 * i; rotatorWayPoints.push([bboxCenter.x + Math.sin(angle) * 100, bboxCenter.y - Math.cos(angle) * 100]); rotatorWayPoints.push([bboxCenter.x + Math.sin(angle) * 1000, bboxCenter.y - Math.cos(angle) * 1000]); // Rotate clockwise by 45 * i degrees AtImageView.drawThroughPoints(rotatorWayPoints, "steps", 10); AtOutliner.seeSelectedRegion(); // Check that rotating was successful const rectangleResult = await LabelStudio.serialize(); Asserts.deepEqualWithTolerance(Math.round(rectangleResult[0].value.rotation), 45 * i); // undo rotation I.pressKey(["CommandOrControl", "z"]); // clear unnecessary waypoints rotatorWayPoints.pop(); } }, );