Skip to content

Commit bfec410

Browse files
niklubnikrobot-ci-heartexhlomzik
authored
feat: Support snap to pixel for RectangleLabels (#7983)
Co-authored-by: nik <[email protected]> Co-authored-by: robot-ci-heartex <[email protected]> Co-authored-by: hlomzik <[email protected]>
1 parent 1c11575 commit bfec410

File tree

8 files changed

+98
-14
lines changed

8 files changed

+98
-14
lines changed

docs/source/includes/tags/choices.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
| [perItem] | <code>boolean</code> | | Use this tag to select a choice for a specific item inside the object instead of the whole object |
1717
| [value] | <code>string</code> | | Task data field containing a list of dynamically loaded choices (see example below) |
1818
| [allowNested] | <code>boolean</code> | | Allow to use `children` field in dynamic choices to nest them. Submitted result will contain array of arrays, every item is a list of values from topmost parent choice down to selected one. |
19+
| [layout] | <code>select</code> \| <code>inline</code> \| <code>vertical</code> | | Layout of the choices: `select` for dropdown/select box format, `inline` for horizontal single row display, `vertical` for vertically stacked display (default) |
1920

docs/source/includes/tags/rectangle.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
| [canRotate] | <code>boolean</code> | <code>true</code> | Whether to show or hide rotation control. Note that the anchor point in the results is different than the anchor point used when rotating with the rotation tool. For more information, see [Rotation](/templates/image_bbox#Rotation). |
1212
| [smart] | <code>boolean</code> | | Show smart tool for interactive pre-annotations |
1313
| [smartOnly] | <code>boolean</code> | | Only show smart tool for interactive pre-annotations |
14+
| [snap] | <code>pixel</code> \| <code>none</code> | <code>none</code> | Snap rectangle to image pixels |
1415

docs/source/includes/tags/rectanglelabels.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
| [strokeColor] | <code>string</code> | | Stroke color in hexadecimal |
1313
| [strokeWidth] | <code>number</code> | <code>1</code> | Width of stroke |
1414
| [canRotate] | <code>boolean</code> | <code>true</code> | Show or hide rotation control. Note that the anchor point in the results is different than the anchor point used when rotating with the rotation tool. For more information, see [Rotation](/templates/image_bbox#Rotation). |
15+
| [snap] | <code>pixel</code> \| <code>none</code> | <code>none</code> | Snap rectangle to image pixels |
1516

1617
### Result parameters
1718

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

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,13 @@ const Model = types
292292

293293
self.height = self.parent.canvasToInternalY(canvasHeight);
294294
}
295-
self.setPositionInternal(self.x, self.y, self.width, self.height, self.rotation);
295+
self.setPosition(
296+
self.parent.internalToCanvasX(self.x),
297+
self.parent.internalToCanvasY(self.y),
298+
self.parent.internalToCanvasX(self.width),
299+
self.parent.internalToCanvasY(self.height),
300+
self.rotation,
301+
);
296302

297303
const areaBBoxCoords = self?.bboxCoords;
298304

@@ -336,13 +342,33 @@ const Model = types
336342
* @param {number} rotation
337343
*/
338344
setPosition(x, y, width, height, rotation) {
339-
self.setPositionInternal(
340-
self.parent.canvasToInternalX(x),
341-
self.parent.canvasToInternalY(y),
342-
self.parent.canvasToInternalX(width),
343-
self.parent.canvasToInternalY(height),
344-
rotation,
345-
);
345+
const internalX = self.parent.canvasToInternalX(x);
346+
const internalY = self.parent.canvasToInternalY(y);
347+
const internalWidth = self.parent.canvasToInternalX(width);
348+
const internalHeight = self.parent.canvasToInternalY(height);
349+
350+
// Apply snap to pixel if enabled
351+
if (self.control?.snap === "pixel") {
352+
// Snap top-left corner
353+
const topLeftPoint = self.control.getSnappedPoint({
354+
x: internalX,
355+
y: internalY,
356+
});
357+
358+
// Snap bottom-right corner
359+
const bottomRightPoint = self.control.getSnappedPoint({
360+
x: internalX + internalWidth,
361+
y: internalY + internalHeight,
362+
});
363+
364+
// Calculate snapped dimensions
365+
const snappedWidth = bottomRightPoint.x - topLeftPoint.x;
366+
const snappedHeight = bottomRightPoint.y - topLeftPoint.y;
367+
368+
self.setPositionInternal(topLeftPoint.x, topLeftPoint.y, snappedWidth, snappedHeight, rotation);
369+
} else {
370+
self.setPositionInternal(internalX, internalY, internalWidth, internalHeight, rotation);
371+
}
346372
},
347373

348374
setScale(x, y) {
@@ -478,6 +504,18 @@ const HtxRectangleView = ({ item, setShapeRef }) => {
478504
t.setAttr("scaleX", 1);
479505
t.setAttr("scaleY", 1);
480506

507+
if (self.control?.snap === "pixel") {
508+
// If snap is enabled, we need to snap the coordinates to the pixel grid -
509+
// Sync Konva shape attributes back to computed canvas coordinates to cause a re-render
510+
// Canvas coordinates are updated in the setPosition method
511+
t.position({
512+
x: item.canvasX,
513+
y: item.canvasY,
514+
width: item.canvasWidth,
515+
height: item.canvasHeight,
516+
});
517+
}
518+
481519
item.notifyDrawingFinished();
482520
};
483521

@@ -494,6 +532,19 @@ const HtxRectangleView = ({ item, setShapeRef }) => {
494532

495533
item.setPosition(t.getAttr("x"), t.getAttr("y"), t.getAttr("width"), t.getAttr("height"), t.getAttr("rotation"));
496534
item.setScale(t.getAttr("scaleX"), t.getAttr("scaleY"));
535+
536+
if (item.control?.snap === "pixel") {
537+
// If snap is enabled, we need to snap the coordinates to the pixel grid -
538+
// Sync Konva shape attributes back to computed canvas coordinates to cause a re-render
539+
// Canvas coordinates are updated in the setPosition method
540+
t.position({
541+
x: item.canvasX,
542+
y: item.canvasY,
543+
width: item.canvasWidth,
544+
height: item.canvasHeight,
545+
});
546+
}
547+
497548
item.annotation.history.unfreeze(item.id);
498549

499550
item.notifyDrawingFinished();

web/libs/editor/src/tags/control/Choices.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import { Select, Tooltip } from "@humansignal/ui";
9292
* @param {boolean} [perItem] - Use this tag to select a choice for a specific item inside the object instead of the whole object
9393
* @param {string} [value] - Task data field containing a list of dynamically loaded choices (see example below)
9494
* @param {boolean} [allowNested] - Allow to use `children` field in dynamic choices to nest them. Submitted result will contain array of arrays, every item is a list of values from topmost parent choice down to selected one.
95+
* @param {select|inline|vertical} [layout] - Layout of the choices: `select` for dropdown/select box format, `inline` for horizontal single row display, `vertical` for vertically stacked display (default)
9596
*/
9697
const TagAttrs = types.model({
9798
toname: types.maybeNull(types.string),

web/libs/editor/src/tags/control/Rectangle.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { ToolManagerMixin } from "../../mixins/ToolManagerMixin";
3131
* @param {boolean=} [canRotate=true] - Whether to show or hide rotation control. Note that the anchor point in the results is different than the anchor point used when rotating with the rotation tool. For more information, see [Rotation](/templates/image_bbox#Rotation).
3232
* @param {boolean} [smart] - Show smart tool for interactive pre-annotations
3333
* @param {boolean} [smartOnly] - Only show smart tool for interactive pre-annotations
34+
* @param {pixel|none} [snap=none] - Snap rectangle to image pixels
3435
*/
3536
const TagAttrs = types.model({
3637
toname: types.maybeNull(types.string),
@@ -43,6 +44,7 @@ const TagAttrs = types.model({
4344
fillopacity: types.maybeNull(customTypes.range()),
4445

4546
canrotate: types.optional(types.boolean, true),
47+
snap: types.optional(types.string, "none"),
4648
});
4749

4850
const Model = types

web/libs/editor/src/tags/control/RectangleLabels.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import ControlBase from "./Base";
3939
* @param {string} [strokeColor] - Stroke color in hexadecimal
4040
* @param {number} [strokeWidth=1] - Width of stroke
4141
* @param {boolean} [canRotate=true] - Show or hide rotation control. Note that the anchor point in the results is different than the anchor point used when rotating with the rotation tool. For more information, see [Rotation](/templates/image_bbox#Rotation).
42+
* @param {pixel|none} [snap=none] - Snap rectangle to image pixels
4243
*/
4344

4445
const Validation = types.model({

web/libs/editor/src/tools/Rect.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,39 @@ const _BaseNPointTool = types
6464
},
6565
};
6666
})
67-
.actions((self) => ({
68-
beforeCommitDrawing() {
69-
const s = self.getActiveShape;
67+
.actions((self) => {
68+
const Super = {
69+
commitDrawingRegion: self.commitDrawingRegion,
70+
};
7071

71-
return s.width > self.MIN_SIZE.X && s.height * self.MIN_SIZE.Y;
72-
},
73-
}));
72+
return {
73+
beforeCommitDrawing() {
74+
const s = self.getActiveShape;
75+
76+
return s.width > self.MIN_SIZE.X && s.height > self.MIN_SIZE.Y;
77+
},
78+
79+
commitDrawingRegion() {
80+
const { currentArea, control, obj } = self;
81+
82+
if (!currentArea) return;
83+
84+
// Apply snap to pixel if enabled before finalizing the region
85+
if (control?.snap === "pixel") {
86+
const canvasX = currentArea.parent.internalToCanvasX(currentArea.x);
87+
const canvasY = currentArea.parent.internalToCanvasY(currentArea.y);
88+
const canvasWidth = currentArea.parent.internalToCanvasX(currentArea.width);
89+
const canvasHeight = currentArea.parent.internalToCanvasY(currentArea.height);
90+
91+
// Apply snap logic through setPosition which handles both corners
92+
currentArea.setPosition(canvasX, canvasY, canvasWidth, canvasHeight, currentArea.rotation);
93+
}
94+
95+
// Use the parent commitDrawingRegion to finalize the region
96+
return Super.commitDrawingRegion();
97+
},
98+
};
99+
});
74100

75101
const _Tool = types
76102
.model("RectangleTool", {

0 commit comments

Comments
 (0)