diff --git a/web_ui/packages/smart-tools/index.ts b/web_ui/packages/smart-tools/index.ts index ba1f0c132b..ba53bdfeac 100644 --- a/web_ui/packages/smart-tools/index.ts +++ b/web_ui/packages/smart-tools/index.ts @@ -16,3 +16,4 @@ export { buildIntelligentScissorsInstance, IntelligentScissors } from './src/int export { Anchor } from './src/edit-bounding-box/anchor.component'; export { ANCHOR_SIZE, ResizeAnchor } from './src/edit-bounding-box/resize-anchor.component'; +export { EditBoundingBox } from './src/edit-bounding-box/edit-bounding-box/edit-bounding-box.component'; diff --git a/web_ui/packages/smart-tools/src/css-modules.d.ts b/web_ui/packages/smart-tools/src/css-modules.d.ts new file mode 100644 index 0000000000..06005f3c7b --- /dev/null +++ b/web_ui/packages/smart-tools/src/css-modules.d.ts @@ -0,0 +1,7 @@ +// Copyright (C) 2022-2025 Intel Corporation +// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE + +declare module '*.module.scss' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.component.tsx b/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.component.tsx index 6c53e4c7ff..04c5f34765 100644 --- a/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.component.tsx +++ b/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.component.tsx @@ -6,19 +6,7 @@ import { CSSProperties, PointerEvent, ReactNode, useState } from 'react'; import { isFunction } from 'lodash-es'; import { Point } from '../shared/interfaces'; - -interface MouseButton { - button: number; - buttons: number; -} - -const BUTTON_LEFT = { - button: 0, - buttons: 1, -}; -const isLeftButton = (button: MouseButton): boolean => { - return button.button === BUTTON_LEFT.button || button.buttons === BUTTON_LEFT.buttons; -}; +import { isLeftButton } from '../utils/mouse-utils'; interface AnchorProps { children: ReactNode; diff --git a/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.component.tsx b/web_ui/packages/smart-tools/src/edit-bounding-box/edit-bounding-box/edit-bounding-box.component.tsx similarity index 65% rename from web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.component.tsx rename to web_ui/packages/smart-tools/src/edit-bounding-box/edit-bounding-box/edit-bounding-box.component.tsx index 332f67103b..d80e940afe 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.component.tsx +++ b/web_ui/packages/smart-tools/src/edit-bounding-box/edit-bounding-box/edit-bounding-box.component.tsx @@ -3,20 +3,15 @@ import { useEffect, useState } from 'react'; -import { ANCHOR_SIZE, ResizeAnchor } from '@geti/smart-tools'; -import { RegionOfInterest } from '@geti/smart-tools/types'; - -import { Annotation } from '../../../../../core/annotations/annotation.interface'; -import { Point } from '../../../../../core/annotations/shapes.interface'; -import { ShapeType } from '../../../../../core/annotations/shapetype.enum'; -import { Labels } from '../../../annotation/labels/labels.component'; +import { Annotation, Point, RegionOfInterest } from '../../shared/interfaces'; +import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from '../../utils/tool-utils'; +import { ANCHOR_SIZE, ResizeAnchor } from '../resize-anchor.component'; import { TranslateShape } from '../translate-shape.component'; -import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from '../utils'; -import classes from './../../../annotator-canvas.module.scss'; +import classes from './edit-bounding-box.module.scss'; interface EditBoundingBoxProps { - annotation: Annotation & { shape: { shapeType: ShapeType.Rect } }; + annotation: Annotation & { shape: { shapeType: 'rect' } }; disableTranslation?: boolean; disablePoints?: boolean; roi: RegionOfInterest; @@ -36,6 +31,8 @@ export const EditBoundingBox = ({ }: EditBoundingBoxProps): JSX.Element => { const [shape, setShape] = useState(annotation.shape); + const ariaLabel = `${annotation.isSelected ? 'Selected' : 'Not selected'} shape ${annotation.id}`; + useEffect(() => setShape(annotation.shape), [annotation.shape]); const onComplete = () => { @@ -70,11 +67,29 @@ export const EditBoundingBox = ({ annotation={{ ...annotation, shape }} translateShape={translate} onComplete={onComplete} - /> + > + + + + - - {disablePoints === false ? ( , + children, }: TranslateShapeProps): JSX.Element => { const [dragFromPoint, setDragFromPoint] = useState(null); @@ -36,7 +32,7 @@ export const TranslateShape = ({ return; } - if (event.pointerType === PointerType.Touch || !isLeftButton(event)) { + if (event.pointerType === 'touch' || !isLeftButton(event)) { return; } @@ -67,7 +63,9 @@ export const TranslateShape = ({ if (dragFromPoint === null) { return; } + event.preventDefault(); + setDragFromPoint(null); event.currentTarget.releasePointerCapture(event.pointerId); onComplete(); diff --git a/web_ui/packages/smart-tools/src/shared/interfaces.ts b/web_ui/packages/smart-tools/src/shared/interfaces.ts index bc2e5990bb..938a65a44d 100644 --- a/web_ui/packages/smart-tools/src/shared/interfaces.ts +++ b/web_ui/packages/smart-tools/src/shared/interfaces.ts @@ -52,3 +52,13 @@ export interface Vector { x: number; y: number; } + +export interface Annotation { + readonly id: string; + readonly shape: Shape; + readonly zIndex: number; + readonly isSelected: boolean; + readonly isHidden: boolean; + readonly isLocked: boolean; + readonly color?: string; +} diff --git a/web_ui/packages/smart-tools/src/utils/index.ts b/web_ui/packages/smart-tools/src/utils/index.ts index eba317b652..f1df25b08a 100644 --- a/web_ui/packages/smart-tools/src/utils/index.ts +++ b/web_ui/packages/smart-tools/src/utils/index.ts @@ -12,7 +12,11 @@ export { isPolygonValid, getPointsFromMat, getMatFromPoints, + getBoundingBoxInRoi, + getBoundingBoxResizePoints, + getClampedBoundingBox, } from './tool-utils'; +export { isLeftButton, isWheelButton, allowPanning } from './mouse-utils'; export * as Vec2 from './vec2'; export { degreesToRadians, diff --git a/web_ui/packages/smart-tools/src/utils/mouse-utils.ts b/web_ui/packages/smart-tools/src/utils/mouse-utils.ts new file mode 100644 index 0000000000..e0f3f59b08 --- /dev/null +++ b/web_ui/packages/smart-tools/src/utils/mouse-utils.ts @@ -0,0 +1,46 @@ +// Copyright (C) 2022-2025 Intel Corporation +// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE + +import { PointerEvent, SVGProps } from 'react'; + +interface MouseButton { + button: number; + buttons: number; +} + +const BUTTON_LEFT = { + button: 0, + buttons: 1, +}; +const BUTTON_WHEEL = { + button: 1, + buttons: 4, +}; + +const isButton = (button: MouseButton, buttonToCompare: MouseButton): boolean => + button.button === buttonToCompare.button || button.buttons === buttonToCompare.buttons; + +export const isLeftButton = (button: MouseButton): boolean => { + return button.button === BUTTON_LEFT.button || button.buttons === BUTTON_LEFT.buttons; +}; + +export const isWheelButton = (button: MouseButton): boolean => { + return isButton(button, BUTTON_WHEEL); +}; + +type OnPointerDown = SVGProps['onPointerDown']; +export const allowPanning = (onPointerDown?: OnPointerDown): OnPointerDown | undefined => { + if (onPointerDown === undefined) { + return; + } + + return (event: PointerEvent) => { + const isPressingPanningHotKeys = (isLeftButton(event) && event.ctrlKey) || isWheelButton(event); + + if (isPressingPanningHotKeys) { + return; + } + + return onPointerDown(event); + }; +}; diff --git a/web_ui/src/pages/annotator/tools/edit-tool/utils.test.ts b/web_ui/packages/smart-tools/src/utils/tool-utils.test.ts similarity index 99% rename from web_ui/src/pages/annotator/tools/edit-tool/utils.test.ts rename to web_ui/packages/smart-tools/src/utils/tool-utils.test.ts index 2e85188f68..bf61a500ad 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/utils.test.ts +++ b/web_ui/packages/smart-tools/src/utils/tool-utils.test.ts @@ -1,7 +1,7 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from './utils'; +import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from './tool-utils'; const mockedRoi = { x: 0, diff --git a/web_ui/packages/smart-tools/src/utils/tool-utils.ts b/web_ui/packages/smart-tools/src/utils/tool-utils.ts index fb946aa179..4d874ab2d6 100644 --- a/web_ui/packages/smart-tools/src/utils/tool-utils.ts +++ b/web_ui/packages/smart-tools/src/utils/tool-utils.ts @@ -4,7 +4,8 @@ import Clipper from '@doodle3d/clipper-js'; import { OpenCVTypes } from '../opencv/interfaces'; -import { Point, Polygon } from '../shared/interfaces'; +import { Point, Polygon, RegionOfInterest } from '../shared/interfaces'; +import { BoundingBox, clampBetween } from './math'; export const formatContourToPoints = ( mask: OpenCVTypes.Mat, @@ -151,3 +152,162 @@ export const getMatFromPoints = (CV: OpenCVTypes.cv, points: Point[], offset = { return pointsMat; }; + +interface getBoundingBoxResizePointsProps { + gap: number; + boundingBox: BoundingBox; + onResized: (boundingBox: BoundingBox) => void; +} + +export const getClampedBoundingBox = (point: Point, boundingBox: RegionOfInterest, roi: RegionOfInterest) => { + const roiX = roi.width + roi.x; + const roiY = roi.height + roi.y; + const shapeX = boundingBox.width + boundingBox.x; + const shapeY = boundingBox.height + boundingBox.y; + + const clampedTranslate = { + x: clampBetween(shapeX - roiX, -point.x, boundingBox.x - roi.x), + y: clampBetween(shapeY - roiY, -point.y, boundingBox.y - roi.y), + }; + + return { + ...boundingBox, + x: boundingBox.x - clampedTranslate.x, + y: boundingBox.y - clampedTranslate.y, + }; +}; + +export const getBoundingBoxInRoi = (boundingBox: BoundingBox, roi: RegionOfInterest) => { + const x = Math.max(0, boundingBox.x); + const y = Math.max(0, boundingBox.y); + + return { + x, + y, + width: Math.min(roi.width - x, boundingBox.width), + height: Math.min(roi.height - y, boundingBox.height), + }; +}; + +// Keep a gap between anchor points so that they don't overlap +export const getBoundingBoxResizePoints = ({ boundingBox, gap, onResized }: getBoundingBoxResizePointsProps) => { + return [ + { + x: boundingBox.x, + y: boundingBox.y, + moveAnchorTo: (x: number, y: number) => { + const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); + const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); + + onResized({ + x: x1, + width: Math.max(gap, boundingBox.width + boundingBox.x - x1), + y: y1, + height: Math.max(gap, boundingBox.height + boundingBox.y - y1), + }); + }, + cursor: 'nw-resize', + label: 'North west resize anchor', + }, + { + x: boundingBox.x + boundingBox.width / 2, + y: boundingBox.y, + moveAnchorTo: (_x: number, y: number) => { + const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); + + onResized({ + ...boundingBox, + y: y1, + height: Math.max(gap, boundingBox.height + boundingBox.y - y1), + }); + }, + cursor: 'n-resize', + label: 'North resize anchor', + }, + { + x: boundingBox.x + boundingBox.width, + y: boundingBox.y, + moveAnchorTo: (x: number, y: number) => { + const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); + + onResized({ + ...boundingBox, + width: Math.max(gap, x - boundingBox.x), + y: y1, + height: Math.max(gap, boundingBox.height + boundingBox.y - y1), + }); + }, + cursor: 'ne-resize', + label: 'North east resize anchor', + }, + { + x: boundingBox.x + boundingBox.width, + y: boundingBox.y + boundingBox.height / 2, + moveAnchorTo: (x: number) => { + onResized({ ...boundingBox, width: Math.max(gap, x - boundingBox.x) }); + }, + cursor: 'e-resize', + label: 'East resize anchor', + }, + { + x: boundingBox.x + boundingBox.width, + y: boundingBox.y + boundingBox.height, + moveAnchorTo: (x: number, y: number) => { + onResized({ + x: boundingBox.x, + width: Math.max(gap, x - boundingBox.x), + + y: boundingBox.y, + height: Math.max(gap, y - boundingBox.y), + }); + }, + cursor: 'se-resize', + label: 'South east resize anchor', + }, + { + x: boundingBox.x + boundingBox.width / 2, + y: boundingBox.y + boundingBox.height, + moveAnchorTo: (_x: number, y: number) => { + onResized({ + ...boundingBox, + y: boundingBox.y, + height: Math.max(gap, y - boundingBox.y), + }); + }, + cursor: 's-resize', + label: 'South resize anchor', + }, + { + x: boundingBox.x, + y: boundingBox.y + boundingBox.height, + moveAnchorTo: (x: number, y: number) => { + const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); + + onResized({ + x: x1, + width: Math.max(gap, boundingBox.width + boundingBox.x - x1), + + y: boundingBox.y, + height: Math.max(gap, y - boundingBox.y), + }); + }, + cursor: 'sw-resize', + label: 'South west resize anchor', + }, + { + x: boundingBox.x, + y: boundingBox.y + boundingBox.height / 2, + moveAnchorTo: (x: number, _y: number) => { + const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); + + onResized({ + ...boundingBox, + x: x1, + width: Math.max(gap, boundingBox.width + boundingBox.x - x1), + }); + }, + cursor: 'w-resize', + label: 'West resize anchor', + }, + ]; +}; diff --git a/web_ui/packages/smart-tools/tsconfig.json b/web_ui/packages/smart-tools/tsconfig.json index 7248ff3128..d6baa698e3 100644 --- a/web_ui/packages/smart-tools/tsconfig.json +++ b/web_ui/packages/smart-tools/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@geti/config/typescript", "include": [ - "src" + "src", + "./src/css-modules.d.ts" ], "exclude": [ "./src/opencv/4.9.0" diff --git a/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.test.tsx b/web_ui/src/pages/annotator/tools/edit-tool/anchor.test.tsx similarity index 97% rename from web_ui/packages/smart-tools/src/edit-bounding-box/anchor.test.tsx rename to web_ui/src/pages/annotator/tools/edit-tool/anchor.test.tsx index 948faedacd..6af7a5f494 100644 --- a/web_ui/packages/smart-tools/src/edit-bounding-box/anchor.test.tsx +++ b/web_ui/src/pages/annotator/tools/edit-tool/anchor.test.tsx @@ -3,10 +3,9 @@ import '@wessberg/pointer-events'; +import { Anchor } from '@geti/smart-tools'; import { fireEvent, render, screen } from '@testing-library/react'; -import { Anchor } from './anchor.component'; - describe('anchor', (): void => { const properties = { x: 10, diff --git a/web_ui/src/pages/annotator/tools/edit-tool/edit-annotation-tool.component.tsx b/web_ui/src/pages/annotator/tools/edit-tool/edit-annotation-tool.component.tsx index 8a68ca1162..c66efc1679 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/edit-annotation-tool.component.tsx +++ b/web_ui/src/pages/annotator/tools/edit-tool/edit-annotation-tool.component.tsx @@ -3,12 +3,15 @@ import { RefObject, useRef } from 'react'; +import { EditBoundingBox as EditBoundingBoxTool } from '@geti/smart-tools'; + import { Annotation, KeypointAnnotation } from '../../../../core/annotations/annotation.interface'; import { ShapeType } from '../../../../core/annotations/shapetype.enum'; import { useOutsideClick } from '../../../../hooks/outside-click/outside-click.hook'; import { hasEqualId } from '../../../../shared/utils'; import { isWheelButton } from '../../../buttons-utils'; import { Labels } from '../../annotation/labels/labels.component'; +import { getLabelsColor } from '../../annotation/labels/utils'; import { ToolType } from '../../core/annotation-tool-context.interface'; import { useROI } from '../../providers/region-of-interest-provider/region-of-interest-provider.component'; import { getGlobalAnnotations } from '../../providers/task-chain-provider/utils'; @@ -16,7 +19,7 @@ import { useTask } from '../../providers/task-provider/task-provider.component'; import { useZoom } from '../../zoom/zoom-provider.component'; import { SelectingToolType } from '../selecting-tool/selecting-tool.enums'; import { ToolAnnotationContextProps } from '../tools.interface'; -import { EditBoundingBox as EditBoundingBoxTool } from './edit-bounding-box/edit-bounding-box.component'; +import { convertToolShapeToGetiShape } from '../utils'; import { EditCircle as EditCircleTool } from './edit-circle/edit-circle.component'; import { EditKeypointTool } from './edit-keypoint/edit-keypoint-tool.component'; import { EditPolygon as EditPolygonTool } from './edit-polygon/edit-polygon.component'; @@ -49,13 +52,25 @@ const EditAnnotationToolFactory = ({ switch (annotation.shape.shapeType) { case ShapeType.Rect: { + const annotationColor = getLabelsColor(annotation.labels, selectedTask); + return ( { + scene.updateAnnotation({ + ...newAnnotation, + shape: convertToolShapeToGetiShape(newAnnotation.shape), + labels: annotation.labels, + }); + }} + annotation={ + { ...annotation, color: annotationColor } as Annotation & { + shape: { shapeType: ShapeType.Rect }; + } + } disableTranslation={disableTranslation} disablePoints={disablePoints} /> diff --git a/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.test.tsx b/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box.test.tsx similarity index 89% rename from web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.test.tsx rename to web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box.test.tsx index fa28cbaf6f..26059ffc53 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box/edit-bounding-box.test.tsx +++ b/web_ui/src/pages/annotator/tools/edit-tool/edit-bounding-box.test.tsx @@ -3,15 +3,13 @@ import '@wessberg/pointer-events'; -import { ANCHOR_SIZE } from '@geti/smart-tools'; +import { ANCHOR_SIZE, EditBoundingBox as EditBoundingBoxTool } from '@geti/smart-tools'; +import { Annotation, RegionOfInterest } from '@geti/smart-tools/types'; import { fireEvent, screen } from '@testing-library/react'; -import { Annotation, RegionOfInterest } from '../../../../../core/annotations/annotation.interface'; -import { ShapeType } from '../../../../../core/annotations/shapetype.enum'; -import { getMockedImage, getMockedROI } from '../../../../../test-utils/utils'; -import { AnnotationToolProvider } from '../../../providers/annotation-tool-provider/annotation-tool-provider.component'; -import { annotatorRender as render } from '../../../test-utils/annotator-render'; -import { EditBoundingBox as EditBoundingBoxTool } from './edit-bounding-box.component'; +import { getMockedImage, getMockedROI } from '../../../../test-utils/utils'; +import { AnnotationToolProvider } from '../../providers/annotation-tool-provider/annotation-tool-provider.component'; +import { annotatorRender as render } from '../../test-utils/annotator-render'; const mockROI = getMockedROI({ x: 0, y: 0, width: 1000, height: 1000 }); const mockImage = getMockedImage(mockROI); @@ -21,7 +19,7 @@ const zoom = 2.0; const renderApp = async ( annotation: Annotation & { shape: { - shapeType: ShapeType.Rect; + shapeType: 'rect'; }; }, roi?: RegionOfInterest @@ -45,7 +43,7 @@ describe('EditRectangleTool', (): void => { const annotation = { id: 'rect-1', labels: [], - shape: { shapeType: ShapeType.Rect as const, x: 10, y: 10, width: 300, height: 200 }, + shape: { shapeType: 'rect' as const, x: 10, y: 10, width: 300, height: 200 }, zIndex: 0, isHovered: false, isSelected: false, @@ -106,7 +104,7 @@ describe('EditRectangleTool', (): void => { id: 'rect-1', labels: [], shape: { - shapeType: ShapeType.Rect as const, + shapeType: 'rect' as const, x: iRoi.x, y: iRoi.y, width: iRoi.width / 2, @@ -207,7 +205,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x + 10, y: shape.y + 10, width: shape.width - 10, @@ -219,7 +217,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x - 10, y: shape.y - 10, width: shape.width + 10, @@ -231,7 +229,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width / 2, y: shape.y }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y + 10, width: shape.width, @@ -243,7 +241,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y - 10, width: shape.width, @@ -255,7 +253,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y + 10, width: shape.width + 10, @@ -267,7 +265,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y - 10, width: shape.width - 10, @@ -279,7 +277,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height / 2 }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width + 10, @@ -291,7 +289,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height / 2 }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width - 10, @@ -303,7 +301,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width + 10, @@ -315,7 +313,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width - 10, @@ -327,7 +325,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width / 2, y: shape.y + shape.height }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width, @@ -339,7 +337,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width / 2, y: shape.y + shape.height }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width, @@ -351,7 +349,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x + 10, y: shape.y, width: shape.width - 10, @@ -363,7 +361,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x - 10, y: shape.y, width: shape.width + 10, @@ -375,7 +373,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height / 2 }, { x: 10, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x + 10, y: shape.y, width: shape.width - 10, @@ -387,7 +385,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height / 2 }, { x: -10, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x - 10, y: shape.y, width: shape.width + 10, @@ -400,7 +398,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y }, { x: -20, y: -20 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: 0, y: 0, width: shape.width + shape.x, @@ -412,7 +410,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width / 2, y: shape.y }, { x: 10, y: -20 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: 0, width: shape.width, @@ -424,7 +422,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y }, { x: mockROI.width, y: -10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: 0, width: mockROI.width - shape.x, @@ -436,7 +434,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height / 2 }, { x: mockROI.width, y: 10 }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: mockROI.width - shape.x, @@ -448,7 +446,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width, y: shape.y + shape.height }, { x: mockROI.width, y: mockROI.height }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: mockROI.width - shape.x, @@ -460,7 +458,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x + shape.width / 2, y: shape.y + shape.height }, { x: 10, y: mockROI.height }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x, y: shape.y, width: shape.width, @@ -472,7 +470,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height }, { x: -mockROI.width, y: mockROI.height }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: 0, y: shape.y, width: shape.width + shape.x, @@ -484,7 +482,7 @@ describe('EditRectangleTool', (): void => { { x: shape.x, y: shape.y + shape.height / 2 }, { x: -mockROI.width, y: mockROI.height }, { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: 0, y: shape.y, width: shape.width + shape.x, @@ -527,7 +525,7 @@ describe('EditRectangleTool', (): void => { const gap = (2 * ANCHOR_SIZE) / zoom; const startPoint = { x: shape.x, y: shape.y }; const expectedRect = { - shapeType: ShapeType.Rect, + shapeType: 'rect', x: shape.x + shape.width - gap, y: shape.y + shape.height - gap, width: gap, diff --git a/web_ui/src/pages/annotator/tools/edit-tool/edit-circle/edit-circle.component.tsx b/web_ui/src/pages/annotator/tools/edit-tool/edit-circle/edit-circle.component.tsx index a7577731fa..f27752a55d 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/edit-circle/edit-circle.component.tsx +++ b/web_ui/src/pages/annotator/tools/edit-tool/edit-circle/edit-circle.component.tsx @@ -6,17 +6,17 @@ import { useEffect, useState } from 'react'; import { ANCHOR_SIZE, ResizeAnchor } from '@geti/smart-tools'; import { Vec2 } from '@geti/smart-tools/utils'; -import { Annotation, RegionOfInterest } from '../../../../../core/annotations/annotation.interface'; +import { TranslateShape } from '../../../../../../packages/smart-tools/src/edit-bounding-box/translate-shape.component'; +import { Annotation as AnnotationType, RegionOfInterest } from '../../../../../core/annotations/annotation.interface'; import { Point } from '../../../../../core/annotations/shapes.interface'; import { ShapeType } from '../../../../../core/annotations/shapetype.enum'; -import { Labels } from '../../../annotation/labels/labels.component'; +import { Annotation } from '../../../annotation/annotation.component'; import { AnnotationToolContext } from '../../../core/annotation-tool-context.interface'; import { useROI } from '../../../providers/region-of-interest-provider/region-of-interest-provider.component'; import { useZoom } from '../../../zoom/zoom-provider.component'; import { getMaxCircleRadius, MIN_RADIUS } from '../../circle-tool/utils'; import { isShapeWithinRoi } from '../../utils'; import { ResizeAnchorType } from '../resize-anchor.enum'; -import { TranslateShape } from '../translate-shape.component'; import classes from './../../../annotator-canvas.module.scss'; @@ -25,7 +25,7 @@ type Circle = { x: number; y: number; r: number }; interface EditCircleProps { annotationToolContext: AnnotationToolContext; - annotation: Annotation & { shape: { shapeType: ShapeType.Circle } }; + annotation: AnnotationType & { shape: { shapeType: ShapeType.Circle } }; disableTranslation?: boolean; disablePoints?: boolean; } @@ -119,7 +119,9 @@ export const EditCircle = ({ annotation={{ ...annotation, shape }} translateShape={translate} onComplete={onComplete} - /> + > + + - - {disablePoints === false ? ( { +const updateOrRemovePolygonAnnotation = (annotation: AnnotationType, scene: AnnotationScene): void => { if (isPolygonValid(annotation.shape as Polygon)) { scene.updateAnnotation({ ...annotation }); } else { @@ -122,11 +122,11 @@ export const EditPolygon = ({ annotation={{ ...annotation, shape }} onComplete={() => onComplete(shape)} disabled={disableTranslation || isBrushSubTool} - /> + > + + - {shape.points.length > 0 && !isBrushSubTool && } - {disablePoints === false && !isBrushSubTool ? ( + > + + {disablePoints === false ? ( diff --git a/web_ui/src/pages/annotator/tools/edit-tool/edit-tool.component.tsx b/web_ui/src/pages/annotator/tools/edit-tool/edit-tool.component.tsx index 2b248435c4..9f1e1d938a 100644 --- a/web_ui/src/pages/annotator/tools/edit-tool/edit-tool.component.tsx +++ b/web_ui/src/pages/annotator/tools/edit-tool/edit-tool.component.tsx @@ -1,8 +1,9 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { RefObject } from 'react'; +import { Fragment, RefObject } from 'react'; +import { Labels } from '../../annotation/labels/labels.component'; import { useAnnotatorMode } from '../../hooks/use-annotator-mode'; import { getOutputFromTask } from '../../providers/task-chain-provider/utils'; import { useTask } from '../../providers/task-provider/task-provider.component'; @@ -41,16 +42,18 @@ export const EditTool = ({ return (
- {annotations.map((annotation) => { + {annotations.map((annotation, index) => { return ( - + + + + ); })}
diff --git a/web_ui/src/pages/annotator/tools/edit-tool/utils.ts b/web_ui/src/pages/annotator/tools/edit-tool/utils.ts deleted file mode 100644 index 079ca5c761..0000000000 --- a/web_ui/src/pages/annotator/tools/edit-tool/utils.ts +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (C) 2022-2025 Intel Corporation -// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE - -import { BoundingBox, clampBetween } from '@geti/smart-tools/utils'; - -import { RegionOfInterest } from '../../../../core/annotations/annotation.interface'; -import { Point } from '../../../../core/annotations/shapes.interface'; - -interface getBoundingBoxResizePointsProps { - gap: number; - boundingBox: BoundingBox; - onResized: (boundingBox: BoundingBox) => void; -} - -export const getClampedBoundingBox = (point: Point, boundingBox: RegionOfInterest, roi: RegionOfInterest) => { - const roiX = roi.width + roi.x; - const roiY = roi.height + roi.y; - const shapeX = boundingBox.width + boundingBox.x; - const shapeY = boundingBox.height + boundingBox.y; - - const clampedTranslate = { - x: clampBetween(shapeX - roiX, -point.x, boundingBox.x - roi.x), - y: clampBetween(shapeY - roiY, -point.y, boundingBox.y - roi.y), - }; - - return { - ...boundingBox, - x: boundingBox.x - clampedTranslate.x, - y: boundingBox.y - clampedTranslate.y, - }; -}; - -export const getBoundingBoxInRoi = (boundingBox: BoundingBox, roi: RegionOfInterest) => { - const x = Math.max(0, boundingBox.x); - const y = Math.max(0, boundingBox.y); - - return { - x, - y, - width: Math.min(roi.width - x, boundingBox.width), - height: Math.min(roi.height - y, boundingBox.height), - }; -}; - -// Keep a gap between anchor points so that they don't overlap -export const getBoundingBoxResizePoints = ({ boundingBox, gap, onResized }: getBoundingBoxResizePointsProps) => { - return [ - { - x: boundingBox.x, - y: boundingBox.y, - moveAnchorTo: (x: number, y: number) => { - const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); - const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); - - onResized({ - x: x1, - width: Math.max(gap, boundingBox.width + boundingBox.x - x1), - y: y1, - height: Math.max(gap, boundingBox.height + boundingBox.y - y1), - }); - }, - cursor: 'nw-resize', - label: 'North west resize anchor', - }, - { - x: boundingBox.x + boundingBox.width / 2, - y: boundingBox.y, - moveAnchorTo: (_x: number, y: number) => { - const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); - - onResized({ - ...boundingBox, - y: y1, - height: Math.max(gap, boundingBox.height + boundingBox.y - y1), - }); - }, - cursor: 'n-resize', - label: 'North resize anchor', - }, - { - x: boundingBox.x + boundingBox.width, - y: boundingBox.y, - moveAnchorTo: (x: number, y: number) => { - const y1 = Math.max(0, Math.min(y, boundingBox.y + boundingBox.height - gap)); - - onResized({ - ...boundingBox, - width: Math.max(gap, x - boundingBox.x), - y: y1, - height: Math.max(gap, boundingBox.height + boundingBox.y - y1), - }); - }, - cursor: 'ne-resize', - label: 'North east resize anchor', - }, - { - x: boundingBox.x + boundingBox.width, - y: boundingBox.y + boundingBox.height / 2, - moveAnchorTo: (x: number) => { - onResized({ ...boundingBox, width: Math.max(gap, x - boundingBox.x) }); - }, - cursor: 'e-resize', - label: 'East resize anchor', - }, - { - x: boundingBox.x + boundingBox.width, - y: boundingBox.y + boundingBox.height, - moveAnchorTo: (x: number, y: number) => { - onResized({ - x: boundingBox.x, - width: Math.max(gap, x - boundingBox.x), - - y: boundingBox.y, - height: Math.max(gap, y - boundingBox.y), - }); - }, - cursor: 'se-resize', - label: 'South east resize anchor', - }, - { - x: boundingBox.x + boundingBox.width / 2, - y: boundingBox.y + boundingBox.height, - moveAnchorTo: (_x: number, y: number) => { - onResized({ - ...boundingBox, - y: boundingBox.y, - height: Math.max(gap, y - boundingBox.y), - }); - }, - cursor: 's-resize', - label: 'South resize anchor', - }, - { - x: boundingBox.x, - y: boundingBox.y + boundingBox.height, - moveAnchorTo: (x: number, y: number) => { - const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); - - onResized({ - x: x1, - width: Math.max(gap, boundingBox.width + boundingBox.x - x1), - - y: boundingBox.y, - height: Math.max(gap, y - boundingBox.y), - }); - }, - cursor: 'sw-resize', - label: 'South west resize anchor', - }, - { - x: boundingBox.x, - y: boundingBox.y + boundingBox.height / 2, - moveAnchorTo: (x: number, _y: number) => { - const x1 = Math.max(0, Math.min(x, boundingBox.x + boundingBox.width - gap)); - - onResized({ - ...boundingBox, - x: x1, - width: Math.max(gap, boundingBox.width + boundingBox.x - x1), - }); - }, - cursor: 'w-resize', - label: 'West resize anchor', - }, - ]; -}; diff --git a/web_ui/src/pages/annotator/tools/keypoint-tool/keypoint-tool.component.tsx b/web_ui/src/pages/annotator/tools/keypoint-tool/keypoint-tool.component.tsx index 4c548491fe..6cffd71f15 100644 --- a/web_ui/src/pages/annotator/tools/keypoint-tool/keypoint-tool.component.tsx +++ b/web_ui/src/pages/annotator/tools/keypoint-tool/keypoint-tool.component.tsx @@ -2,8 +2,10 @@ // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE import { ResizeAnchor } from '@geti/smart-tools'; +import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from '@geti/smart-tools/utils'; import { isEmpty, isNil } from 'lodash-es'; +import { TranslateShape } from '../../../../../packages/smart-tools/src/edit-bounding-box/translate-shape.component'; import { RegionOfInterest } from '../../../../core/annotations/annotation.interface'; import { Point } from '../../../../core/annotations/shapes.interface'; import { labelFromUser } from '../../../../core/annotations/utils'; @@ -12,8 +14,6 @@ import { PoseKeypoints } from '../../annotation/shapes/pose-keypoints.component' import { useVisibleAnnotations } from '../../hooks/use-visible-annotations.hook'; import { useROI } from '../../providers/region-of-interest-provider/region-of-interest-provider.component'; import { useZoom } from '../../zoom/zoom-provider.component'; -import { TranslateShape } from '../edit-tool/translate-shape.component'; -import { getBoundingBoxInRoi, getBoundingBoxResizePoints, getClampedBoundingBox } from '../edit-tool/utils'; import { SvgToolCanvas } from '../svg-tool-canvas.component'; import { ToolAnnotationContextProps } from '../tools.interface'; import { CrosshairDrawingBox } from './crosshair-drawing-box.component'; diff --git a/web_ui/src/pages/annotator/tools/svg-tool-canvas.component.tsx b/web_ui/src/pages/annotator/tools/svg-tool-canvas.component.tsx index ad36018556..ad5223b8d1 100644 --- a/web_ui/src/pages/annotator/tools/svg-tool-canvas.component.tsx +++ b/web_ui/src/pages/annotator/tools/svg-tool-canvas.component.tsx @@ -3,9 +3,7 @@ import { FC, PropsWithChildren, RefObject, SVGProps } from 'react'; -import { roiFromImage } from '@geti/smart-tools/utils'; - -import { allowPanning } from './utils'; +import { allowPanning, roiFromImage } from '@geti/smart-tools/utils'; type CanvasProps = SVGProps & { image: ImageData } & { canvasRef?: RefObject }; // This svg component is used to by tools that need to add local listeners that work in diff --git a/web_ui/src/pages/annotator/tools/utils.ts b/web_ui/src/pages/annotator/tools/utils.ts index 3501d66099..2739cbee9d 100644 --- a/web_ui/src/pages/annotator/tools/utils.ts +++ b/web_ui/src/pages/annotator/tools/utils.ts @@ -1,10 +1,8 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { PointerEvent, SVGProps } from 'react'; - -import Clipper from '@doodle3d/clipper-js'; import type ClipperShape from '@doodle3d/clipper-js'; +import Clipper from '@doodle3d/clipper-js'; import { Shape as SmartToolsShape, Circle as ToolCircle, @@ -20,7 +18,6 @@ import { Circle, Point, Polygon, Rect, RotatedRect, Shape } from '../../../core/ import { ShapeType } from '../../../core/annotations/shapetype.enum'; import { isCircle, isPolygon, isPoseShape, isRect, isRotatedRect } from '../../../core/annotations/utils'; import { Label } from '../../../core/labels/label.interface'; -import { isLeftButton, isWheelButton } from '../../buttons-utils'; import { ToolLabel, ToolType } from '../core/annotation-tool-context.interface'; import { PolygonMode } from './polygon-tool/polygon-tool.enum'; @@ -77,23 +74,6 @@ export const drawingStyles = (defaultLabel: Label | null): typeof DEFAULT_ANNOTA }; }; -type OnPointerDown = SVGProps['onPointerDown']; -export const allowPanning = (onPointerDown?: OnPointerDown): OnPointerDown | undefined => { - if (onPointerDown === undefined) { - return; - } - - return (event: PointerEvent) => { - const isPressingPanningHotKeys = (isLeftButton(event) && event.ctrlKey) || isWheelButton(event); - - if (isPressingPanningHotKeys) { - return; - } - - return onPointerDown(event); - }; -}; - export const blurActiveInput = (isFocused: boolean): void => { const element = document.activeElement; diff --git a/web_ui/src/pages/create-project/components/pose-template/canvas/drawing-box.component.tsx b/web_ui/src/pages/create-project/components/pose-template/canvas/drawing-box.component.tsx index 4f74650a41..eea6dcc5ba 100644 --- a/web_ui/src/pages/create-project/components/pose-template/canvas/drawing-box.component.tsx +++ b/web_ui/src/pages/create-project/components/pose-template/canvas/drawing-box.component.tsx @@ -3,8 +3,9 @@ import { useRef } from 'react'; +import { allowPanning } from '@geti/smart-tools/utils'; + import { KeypointNode, Point } from '../../../../../core/annotations/shapes.interface'; -import { allowPanning } from '../../../../annotator/tools/utils'; import { useZoom } from '../../../../annotator/zoom/zoom-provider.component'; import { getRelativePoint, leftMouseButtonHandler } from '../../../../utils'; import { getDefaultLabelStructure } from '../util';