Skip to content

Commit 7d657d0

Browse files
Gondragosnikniklubhlomzik
authored
fix: BROS-255: Fix bbox flipping (#8148)
Co-authored-by: nik <[email protected]> Co-authored-by: niklub <[email protected]> Co-authored-by: Andrew <[email protected]> Co-authored-by: Gondragos <[email protected]>
1 parent 37f747f commit 7d657d0

File tree

3 files changed

+89
-34
lines changed

3 files changed

+89
-34
lines changed

web/libs/editor/src/components/ImageTransformer/ImageTransformer.jsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ export default class TransformerComponent extends Component {
5656
const selectedNodes = [];
5757

5858
selectedRegions.forEach((shape) => {
59-
if (shape.height < 0) {
60-
shape.flipRegion?.();
61-
}
62-
6359
const shapeContainer = stage.findOne((node) => {
6460
return node.hasName(shape.id) && node.parent;
6561
});

web/libs/editor/src/regions/RectRegion.jsx

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Konva from "konva";
12
import { getRoot, isAlive, types } from "mobx-state-tree";
23
import { useContext } from "react";
34
import { Rect } from "react-konva";
@@ -54,6 +55,7 @@ const RectRegionAbsoluteCoordsDEV3793 = types
5455
self.updateAppearenceFromState();
5556
},
5657
setPosition(x, y, width, height, rotation) {
58+
[x, y, width, height, rotation] = self.beforeSetPosition(x, y, width, height, rotation);
5759
self.x = x;
5860
self.y = y;
5961
self.width = width;
@@ -333,6 +335,34 @@ const Model = types
333335
self.rotation = (rotation + 360) % 360;
334336
},
335337

338+
beforeSetPosition(x, y, width, height, rotation) {
339+
// Konva flipping fix
340+
if (height < 0) {
341+
let flippedBack;
342+
// If height is negative, it means it was flipped. We need to correct it
343+
// Negative height also means that it was changed.
344+
// In that case the difference between rotation and current rotation may be only 0 (or 360) and 180 degrees.
345+
// However, as it's not an integer value, we had to check the difference to be sure,
346+
// so 90 and 270 degrees are the safest values to check
347+
const deltaRotation = Math.abs(rotation - self.rotation) % 360;
348+
if (deltaRotation > 90 && deltaRotation < 270) {
349+
// when rotation changes involved, it's a horizontal flip
350+
flippedBack = self.flipBack({ x, y, width, height, rotation }, true);
351+
} else {
352+
// vertical flip
353+
flippedBack = self.flipBack({ x, y, width, height, rotation });
354+
}
355+
[x, y, width, height, rotation] = [
356+
flippedBack.x,
357+
flippedBack.y,
358+
flippedBack.width,
359+
flippedBack.height,
360+
flippedBack.rotation,
361+
];
362+
}
363+
return [x, y, width, height, rotation];
364+
},
365+
336366
/**
337367
* Bounding Box set position on canvas
338368
* @param {number} x
@@ -342,6 +372,7 @@ const Model = types
342372
* @param {number} rotation
343373
*/
344374
setPosition(x, y, width, height, rotation) {
375+
[x, y, width, height, rotation] = self.beforeSetPosition(x, y, width, height, rotation);
345376
const internalX = self.parent.canvasToInternalX(x);
346377
const internalY = self.parent.canvasToInternalY(y);
347378
const internalWidth = self.parent.canvasToInternalX(width);
@@ -362,8 +393,14 @@ const Model = types
362393
});
363394

364395
// Calculate snapped dimensions
365-
const snappedWidth = bottomRightPoint.x - topLeftPoint.x;
366-
const snappedHeight = bottomRightPoint.y - topLeftPoint.y;
396+
let snappedWidth = bottomRightPoint.x - topLeftPoint.x;
397+
let snappedHeight = bottomRightPoint.y - topLeftPoint.y;
398+
399+
// Ensure at least 1 pixel in size after snapping
400+
const minPixelWidth = self.parent?.zoomedPixelSize?.x ?? 1;
401+
const minPixelHeight = self.parent?.zoomedPixelSize?.y ?? 1;
402+
if (snappedWidth < minPixelWidth) snappedWidth = minPixelWidth;
403+
if (snappedHeight < minPixelHeight) snappedHeight = minPixelHeight;
367404

368405
self.setPositionInternal(topLeftPoint.x, topLeftPoint.y, snappedWidth, snappedHeight, rotation);
369406
} else {
@@ -395,27 +432,39 @@ const Model = types
395432
* - when the region is flipped horizontally with no rotation, we fix the rotation back to 0.
396433
* - when the region is flipped vertically, rotation is still 0, we just flip the height.
397434
*/
398-
flipRegion() {
399-
const height = -self.height;
400-
401-
// the most common case, when the region is flipped horizontally with no rotation,
402-
// for this case we are fixing rotation back to 0, that's more intuitive for the user.
403-
if (self.rotation === 180) {
404-
self.height = height;
405-
self.x -= self.width;
406-
self.rotation = 0;
435+
flipBack(attrs, isHorizontalFlip = false) {
436+
// To make it calculable, we need to avoid relative coordinates
437+
let { x, y, width, height, rotation } = attrs;
438+
const radiansRotation = (rotation * Math.PI) / 180;
439+
const transform = new Konva.Transform();
440+
transform.rotate(radiansRotation);
441+
let targetCorner;
442+
443+
if (isHorizontalFlip) {
444+
// When it flips horizontally, it turns the height negative and rotates the region by 180°.
445+
// In general, we want to return a top-right corner to the top-left corner and rotate back
446+
targetCorner = {
447+
x: width,
448+
y: 0,
449+
};
450+
rotation = (rotation + 180) % 360;
407451
} else {
408-
// we need to invert the height and swap top-left and bottom-left corners, but with respect to rotation.
409-
// we'll use tranform from Konva.js to not fight aspect ratio and rotation.
410-
// transform is calculated in canvas coords, so we need to convert coords back and forth.
411-
const transform = self.shapeRef.getAbsoluteTransform();
412-
// bottom-left corner; it's "above" the top-left corner because of inverted height
413-
const { x, y } = transform.point({ x: 0, y: -self.parent.internalToCanvasY(height) });
414-
415-
self.height = height;
416-
self.x = self.parent.canvasToInternalX(x);
417-
self.y = self.parent.canvasToInternalY(y);
452+
// In a vertical flipping case it affects only the height.
453+
// It means that we want to return a bottom-left corner to the top-left corner
454+
targetCorner = {
455+
x: 0,
456+
y: height,
457+
};
418458
}
459+
const offset = transform.point(targetCorner);
460+
461+
return {
462+
x: x + offset.x,
463+
y: y + offset.y,
464+
width,
465+
height: -height,
466+
rotation,
467+
};
419468
},
420469

421470
/**
@@ -492,6 +541,7 @@ const HtxRectangleView = ({ item, setShapeRef }) => {
492541
};
493542
eventHandlers.onTransformEnd = (e) => {
494543
const t = e.target;
544+
const isFlipped = t.getAttr("scaleY") < 0;
495545

496546
item.setPosition(
497547
t.getAttr("x"),
@@ -516,6 +566,12 @@ const HtxRectangleView = ({ item, setShapeRef }) => {
516566
});
517567
}
518568

569+
if (isFlipped) {
570+
// Somehow react-konva caches rotation, most probably as a controllable state,
571+
// so we need to set it manually if it able to be reverted to the previous value
572+
t.setAttr("rotation", item.rotation);
573+
}
574+
519575
item.notifyDrawingFinished();
520576
};
521577

web/libs/editor/tests/e2e/tests/image.transformer.test.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,6 @@ Data(shapesTable.filter(({ shapeName }) => shapeName === "Rectangle")).Scenario(
302302
// Check resulting sizes
303303
rectangleResult = await LabelStudio.serialize();
304304
const exceptedResult = Shape.byBBox(150, 50, 50, 100).result;
305-
306305
Asserts.deepEqualWithTolerance(rectangleResult[0].value, convertToImageSize(exceptedResult));
307306

308307
// new center of the region
@@ -314,21 +313,25 @@ Data(shapesTable.filter(({ shapeName }) => shapeName === "Rectangle")).Scenario(
314313

315314
rectangleResult = await LabelStudio.serialize();
316315
Asserts.deepEqualWithTolerance(rectangleResult[0].value.rotation, 45);
317-
318-
// Flip the shape and keep the width;
319-
// we have to move the rotation handle to the left and down twice the width of the region,
320-
// with respect to 45° rotation
316+
// Flip the shape horizontally with non-zero rotation
321317
const shift = (50 * 2) / Math.SQRT2;
322318
AtImageView.drawByDrag(center[0] - 25 / Math.SQRT2, center[1] - 25 / Math.SQRT2, shift, shift);
323319

324-
const rotatedResult = {
325-
...convertToImageSize(Shape.byBBox(center[0] + 25 / Math.SQRT2, center[1] + 125 / Math.SQRT2, 50, 100).result),
326-
rotation: 180 + 45,
320+
const secondFlipResult = {
321+
...convertToImageSize(
322+
Shape.byBBox(
323+
center[0] + 25 / Math.SQRT2 + 50 / Math.SQRT2,
324+
center[1] + 25 / Math.SQRT2 - 50 / Math.SQRT2,
325+
50,
326+
100,
327+
).result,
328+
),
329+
rotation: 45,
327330
};
328331

329332
rectangleResult = await LabelStudio.serialize();
330333
// flipping is not very precise, so we have to increase the tolerance
331-
Asserts.deepEqualWithTolerance(rectangleResult[0].value, rotatedResult, 0);
334+
Asserts.deepEqualWithTolerance(rectangleResult[0].value, secondFlipResult, 0);
332335
},
333336
);
334337

0 commit comments

Comments
 (0)