const { serialize } = require("./helpers"); const assert = require("assert"); Feature("Zooming and rotating"); const IMAGE = "https://htx-pub.s3.us-east-1.amazonaws.com/examples/images/nick-owuor-astro-nic-visuals-wDifg5xc9Z4-unsplash.jpg"; const BLUEVIOLET = { color: "#8A2BE2", rgbArray: [138, 43, 226], }; const getConfigWithShape = (shape, props = "") => ` <${shape}Labels ${props} name="tag" toName="img"> `; const hScaleCoords = ([x, y], w, h) => { const ratio = w / h; return [x * ratio, y * ratio]; }; const rotateCoords = (point, degree, w, h) => { const [x, y] = point; if (!degree) return point; degree = (360 + degree) % 360; if (degree === 90) return hScaleCoords([h - y - 1, x], w, h); if (degree === 270) return hScaleCoords([y, w - x - 1], w, h); if (Math.abs(degree) === 180) return [w - x - 1, h - y - 1]; return [x, y]; }; const shapes = [ { shape: "KeyPoint", props: 'strokeWidth="5"', action: "clickKonva", regions: [ { params: [100, 100], }, { params: [200, 100], }, ], }, { shape: "Polygon", action: "clickPolygonPointsKonva", regions: [ { params: [ [ [95, 95], [95, 105], [105, 105], [105, 95], ], ], }, { params: [ [ [400, 10], [400, 90], [370, 30], [300, 10], ], ], }, ], }, { shape: "Rectangle", action: "dragKonva", regions: [ { params: [95, 95, 10, 10], }, { params: [400, 350, -50, -50], }, ], }, { shape: "Ellipse", action: "dragKonva", regions: [ { params: [100, 100, 10, 10], }, { params: [230, 300, -50, -30], }, ], }, ]; const shapesTable = new DataTable(["shape", "props", "action", "regions"]); shapes.forEach(({ shape, props = "", action, regions }) => { shapesTable.add([shape, props, action, regions]); }); Data(shapesTable).Scenario( "Simple rotation", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const config = getConfigWithShape(current.shape, current.props); const params = { config, data: { image: IMAGE }, }; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(params); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); const canvasSize = await AtImageView.getCanvasSize(); for (const region of current.regions) { I.pressKey(["u"]); I.pressKey("1"); AtImageView[current.action](...region.params); } const standard = await I.executeScript(serialize); const rotationQueue = ["right", "right", "right", "right", "left", "left", "left", "left"]; let degree = 0; let hasPixel = await AtImageView.hasPixelColor(100, 100, BLUEVIOLET.rgbArray); assert.equal(hasPixel, true); for (const rotate of rotationQueue) { I.click(locate(`[aria-label='rotate-${rotate}']`)); degree += rotate === "right" ? 90 : -90; hasPixel = await AtImageView.hasPixelColor( ...rotateCoords([100, 100], degree, canvasSize.width, canvasSize.height).map(Math.round), BLUEVIOLET.rgbArray, ); assert.strictEqual(hasPixel, true); const result = await I.executeScript(serialize); for (let i = 0; i < standard.length; i++) { assert.deepEqual(standard[i].result, result[i].result); } } }, ); Data(shapesTable).Scenario("Rotate zoomed", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const params = { config: getConfigWithShape(current.shape, current.props), data: { image: IMAGE }, }; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); LabelStudio.init(params); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); const canvasSize = await AtImageView.getCanvasSize(); for (const region of current.regions) { I.pressKey(["u"]); I.pressKey("1"); AtImageView[current.action](...region.params); } const rotationQueue = ["right", "right", "right", "right", "left", "left", "left", "left"]; let degree = 0; const ZOOM = 3; AtImageView.setZoom(ZOOM, -100 * ZOOM, -100 * ZOOM); let hasPixel = await AtImageView.hasPixelColor(1, 1, BLUEVIOLET.rgbArray); assert.strictEqual(hasPixel, true, "Must have pixel before rotation"); for (const rotate of rotationQueue) { I.click(locate(`[aria-label='rotate-${rotate}']`)); degree += rotate === "right" ? 90 : -90; hasPixel = await AtImageView.hasPixelColor( ...rotateCoords([1, 1], degree, canvasSize.width, canvasSize.height).map(Math.round), BLUEVIOLET.rgbArray, ); assert.strictEqual(hasPixel, true, `Must have pixel after rotation [${degree}deg]`); } }); const windowSizesTable = new DataTable(["width", "height"]); windowSizesTable.add([1280, 720]); windowSizesTable.add([1920, 1080]); windowSizesTable.add([800, 480]); windowSizesTable.add([1017, 970]); Data(windowSizesTable).Scenario( "Rotation with different window sizes", async ({ I, LabelStudio, AtImageView, AtOutliner, AtPanels, current }) => { const config = getConfigWithShape("Rectangle"); const params = { config, data: { image: IMAGE }, }; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.amOnPage("/"); I.resizeWindow(current.width, current.height); LabelStudio.init(params); // On small screens you can't see the details panel from the beginning if (current.width > 1000) { AtDetailsPanel.collapsePanel(); } LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(0); const canvasSize = await AtImageView.getCanvasSize(); const imageSize = await AtImageView.getImageFrameSize(); const rotationQueue = ["right", "right", "right", "right", "left", "left", "left", "left"]; assert(Math.abs(canvasSize.width - imageSize.width) < 1); assert(Math.abs(canvasSize.height - imageSize.height) < 1); for (const rotate of rotationQueue) { I.click(locate(`[aria-label='rotate-${rotate}']`)); // Just checking that we see image, to get some time for rotating to be finished and correctly rendered I.seeElement('[alt="LS"]'); I.waitTicks(2); const rotatedCanvasSize = await AtImageView.getCanvasSize(); const rotatedImageSize = await AtImageView.getImageFrameSize(); assert(Math.abs(rotatedCanvasSize.width - rotatedImageSize.width) < 1); assert(Math.abs(rotatedCanvasSize.height - rotatedImageSize.height) < 1); } }, ); const twoColumnsConfigs = [ ` `, ` `, ]; const layoutVariations = new DataTable(["config", "inline", "reversed"]); twoColumnsConfigs.forEach((config) => { for (const inline of [true, false]) { for (const reversed of [true, false]) { layoutVariations.add([config, inline, reversed]); } } }); const compareSize = async (I, AtImageView, message1, message2) => { const { width: canvasWidth, height: canvasHeight } = await AtImageView.getCanvasSize(); const { width: imageWidth, height: imageHeight } = await AtImageView.getImageFrameSize(); const widthMessage = `[${message2}] Check width: [${[canvasWidth, imageWidth]}]`; const heightMessage = `[${message2}] Check height: [${[canvasHeight, imageHeight]}]`; I.say(`${message1} [stage: ${canvasWidth}x${canvasHeight}, image: ${imageWidth}x${imageHeight}]`); assert(Math.abs(canvasWidth - imageWidth) <= 1, widthMessage); assert(Math.abs(canvasHeight - imageHeight) <= 1, heightMessage); }; Data(layoutVariations).Scenario( "Rotation in the two columns template", async ({ I, LabelStudio, AtImageView, AtOutliner, AtSettings, AtPanels, current }) => { I.amOnPage("/"); let isVerticalLayout = false; const { config, inline, reversed } = current; const direction = (inline ? "column" : "row") + (reversed ? "-reverse" : ""); const resultConfig = config.replace("{{direction}}", direction).replace("{{showInline}}", `${inline}`); const params = { config: resultConfig, data: { image: IMAGE }, annotations: [ { id: "rotations", result: [ // The region just for canvas size visually indication { from_name: "label", id: "EUsEHxTyrv", image_rotation: 0, origin: "manual", original_height: 2802, original_width: 2242, to_name: "image", type: "rectanglelabels", value: { height: 100, labels: ["Label 2"], rotation: 0, width: 100, x: 0, y: 0, }, }, ], }, ], }; const AtDetailsPanel = AtPanels.usePanel(AtPanels.PANEL.DETAILS); I.say(`Two columns [config: ${twoColumnsConfigs.indexOf(config)}] [${direction}]`); LabelStudio.init(params); AtDetailsPanel.collapsePanel(); LabelStudio.waitForObjectsReady(); AtOutliner.seeRegions(1); I.click(locate("[aria-label='rotate-right']")); AtOutliner.seeRegions(1); I.wait(0.1); await compareSize(I, AtImageView, "Dimensions must be equal in landscape", "landscape, rotated"); I.say("Change to vertcal layout"); AtSettings.open(); isVerticalLayout = !isVerticalLayout; AtSettings.setLayoutSettings({ [AtSettings.LAYOUT_SETTINGS.VERTICAL_LAYOUT]: isVerticalLayout, }); AtSettings.close(); AtOutliner.seeRegions(1); I.wait(0.1); await compareSize(I, AtImageView, "Dimensions must be equal in portrait", "portrait"); I.click(locate("[aria-label='rotate-right']")); AtOutliner.seeRegions(1); I.wait(0.1); await compareSize(I, AtImageView, "Dimensions must be equal after rotation in portrain", "portrait, rotated"); }, );