diff --git a/README.md b/README.md index 2daf6db1..f883d8f0 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,8 @@ Andrew Ng ## Sneak Peek -### Bounding boxes - -

- bbox -

- -### Points -

- points -

- -### Polygons - -

- polygon + bbox

## Set Up the Project Locally @@ -53,8 +39,23 @@ npm install # serve with hot reload at localhost:3000 npm start ``` +To ensure proper functionality of the application locally, an npm `6.x.x` and node.js `v11.x.x` versions are required. More information about this problem is available in the [#16][4]. + +## Supported Keyboard Shortcuts + +| Functionality | Context | Mac | Windows / Linux | +|:-----------------------------------|:--------:|:---:|:----------------:| +| Polygon autocomplete | Editor | Enter | Enter | +| Cancel polygon drawing | Editor | Escape | Escape | +| Delete currently selected label | Editor | Backspace | Delete | +| Load previous image | Editor | + Left | Ctrl + Left | +| Load next image | Editor | + Right | Ctrl + Right | +| Zoom in | Editor | + + | Ctrl + + | +| Zoom out | Editor | + - | Ctrl + - | +| Move image | Editor | Up / Down / Left / Right | Up / Down / Left / Right | +| Exit popup | Popup | Escape | Escape | -Some Windows 10 users may also have problems with running applications locally. The problems can be solved by adding additional dependencies to the project, through a command: `npm install normalize.css --save`. More information about this problem is available in the [#16][4]. +**Table 1.** Supported keyboard shortcuts ## Supported Output Formats @@ -64,182 +65,35 @@ Some Windows 10 users may also have problems with running applications locally. | **Rect** | ☑ | ☑ | ☑ | ☐ | ☐ | ☒ | | **Polygon** | ☐ | ☒ | ☐ | ☑ | ☐ | ☐ | -**Table 1.** The matrix of supported labels export format, where: +**Table 2.** The matrix of supported labels export format, where: * ☑ - supported format * ☐ - not yet supported format * ☒ - format does not make sense for a given label type +You can find examples of export files along with a description and schema on our [Wiki][7]. +## Privacy -**A .zip package containing files in YOLO format** - -
example of file in YOLO format

- -**Schema:** - -`label_index rel_rect_center_x rel_rect_center_y rel_rect_width rel_rect_height` - -**Where:** - -`label_index` - index of the selected label -`rel_rect_center_x` - horizontal position of the centre of the rect in relation to overall image width, value between [0, 1] -`rel_rect_center_y` - vertical position of the centre of the rect in relation to overall image height, value between [0, 1] -`rel_rect_width` - rect width in relation to overall image width, value between [0, 1] -`rel_rect_height` - rect height in relation to overall image height, value between [0, 1] - -**Example:** - -``` -1 0.404528 0.543963 0.244094 0.727034 -2 0.610236 0.494751 0.188976 0.437008 -1 0.754921 0.791339 0.354331 0.413386 -``` -

- -**A .zip package containing files in Pascal VOC XML format** - -
example of file in Pascal VOC XML format

- -**Schema:** - -```xml - - { project_name } - { image_name } - { /project_name/file_name } - - Unspecified - - - { image_width } - { image_height } - 3 - - - { label_name } - Unspecified - Unspecified - Unspecified - - { rect_left } - { rect_top } - { rect_right } - { rect_bottom } - - - -``` - -**Where:** - -`project_name` - user-defined project name -`image_name` - name of the photo file -`label_name` - selected label name -`rect_left` - absolute horizontal distance between the left edge of the image and the left edge of the rect in pixels -`rect_top` - absolute vertical distance between the top edge of the image and the top edge of the rect in pixels -`rect_right` - absolute horizontal distance between the left edge of the image and the right edge of the rect in pixels -`rect_bottom` - absolute vertical distance between the top edge of the image and the bottom edge of the rect in pixels -`image_width` - absolute image width in pixels -`image_height` - absolute image height in pixels - -**Example:** - -```xml - - my-project-name - 000007.jpg - /my-project-name/000007.jpg - - Unspecified - - - 1280 - 960 - 3 - - - kiwi - Unspecified - Unspecified - Unspecified - - 208 - 486 - 497 - 718 - - - - banaba - Unspecified - Unspecified - Unspecified - - 643 - 118 - 1178 - 799 - - - -``` -

- -**Single CSV file** - -
example of CSV file

+We don't store your images, because we don't send them anywhere in the first place. -**Schema:** +## Road Map -`label_name,rect_left,rect_top,rect_width,rect_height,image_name,image_width,image_height` +Our application is being actively developed. Check out our plans for the near future on our [Wiki][6]. If you have an idea for a new functionality, please hit us on [Twitter][3] and [Gitter][5] or create an issue where you can describe your concept. In the meantime, see what improvements we are planning for you in the future. -**Where:** +## Contribution -`label_name` - selected label name -`rect_left` - absolute horizontal distance between the left edge of the image and the left edge of the rect in pixels -`rect_top` - absolute vertical distance between the top edge of the image and the top edge of the rect in pixels -`rect_width` - absolute rect width in pixels -`rect_height` - absolute rect height in pixels -`image_width` - absolute image width in pixels -`image_height` - absolute image height in pixels +Feel free to file [issues](https://github.com/SkalskiP/make-sense/issues) or [pull requests](https://github.com/SkalskiP/make-sense/pulls). -**Example:** +## Citation ``` -banana,491,164,530,614,000000.jpg,1280,960 -banana,462,245,466,353,000001.jpg,1280,960 -banana,542,477,587,375,000001.jpg,1280,960 -banana,636,109,561,695,000007.jpg,1280,960 -kiwi,198,477,317,251,000007.jpg,1280,960 -kiwi,558,423,219,222,000008.jpg,1280,960 -kiwi,758,360,252,236,000008.jpg,1280,960 +@MISC{make-sense, + author = {Piotr Skalski}, + title = {{Make Sense}}, + howpublished = "\url{https://github.com/SkalskiP/make-sense/}", + year = {2019}, +} ``` -

- -## Privacy - -We don't store your images, because we don't send them anywhere in the first place. - -## Road Map - -Our application is being actively developed. If you have an idea for a new functionality, please hit us on [Twitter][3] and [Gitter][5] or create an issue where you can describe your concept. In the meantime, see what improvements we are planning for you in the future. - -- [X] Export rect labels in Pascal VOC XML format -- [X] Labelling objects using polygons -- [X] Export polygon labels in VGG JSON format -- [ ] Optimization of the process of loading photos from disk - queuing -- [ ] Labelling objects using lines -- [ ] Autofill in label selection dropdown -- [ ] Export labels in COCO JSON format -- [ ] Export segmentation labels as image mask -- [ ] Separate tab with settings -- [ ] Support basic image operations like crop and resize -- [ ] Converting video to image frames -- [ ] Keyboard shortcuts to improve productivity -- [ ] Automatic detection of objects in a photo - all you have to do is to label them -- [ ] OCR labelling -- [ ] Integration with external storage - Amazon S3, Google Drive, Dropbox -- [ ] Copy annotations from previous image into the next one ## Citation @@ -263,3 +117,5 @@ Copyright (c) 2019-present, Piotr Skalski [3]: https://twitter.com/PiotrSkalski92 [4]: https://github.com/SkalskiP/make-sense/issues/16 [5]: https://gitter.im/make-sense-ai/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link +[6]: https://github.com/SkalskiP/make-sense/wiki/Road-Map +[7]: https://github.com/SkalskiP/make-sense/wiki/Supported-Output-Formats diff --git a/examples/alfa-demo.gif b/examples/alfa-demo.gif index 63cbccfe..c0313068 100644 Binary files a/examples/alfa-demo.gif and b/examples/alfa-demo.gif differ diff --git a/public/ico/hand-fill-grab.png b/public/ico/hand-fill-grab.png new file mode 100644 index 00000000..26e0c510 Binary files /dev/null and b/public/ico/hand-fill-grab.png differ diff --git a/public/ico/hand-fill.png b/public/ico/hand-fill.png new file mode 100644 index 00000000..0bf3b2fe Binary files /dev/null and b/public/ico/hand-fill.png differ diff --git a/public/ico/hand.png b/public/ico/hand.png new file mode 100644 index 00000000..d09e34c5 Binary files /dev/null and b/public/ico/hand.png differ diff --git a/public/ico/zoom-fit.png b/public/ico/zoom-fit.png new file mode 100644 index 00000000..0f2c87fd Binary files /dev/null and b/public/ico/zoom-fit.png differ diff --git a/public/ico/zoom-in.png b/public/ico/zoom-in.png new file mode 100644 index 00000000..ce3f624e Binary files /dev/null and b/public/ico/zoom-in.png differ diff --git a/public/ico/zoom-max.png b/public/ico/zoom-max.png new file mode 100644 index 00000000..87bd0690 Binary files /dev/null and b/public/ico/zoom-max.png differ diff --git a/public/ico/zoom-out.png b/public/ico/zoom-out.png new file mode 100644 index 00000000..902ecd4c Binary files /dev/null and b/public/ico/zoom-out.png differ diff --git a/src/App.tsx b/src/App.tsx index 1cb8a25b..cfce5b29 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,21 +6,20 @@ import {ProjectType} from "./data/enums/ProjectType"; import {AppState} from "./store"; import {connect} from "react-redux"; import PopupView from "./views/PopupView/PopupView"; -import {MobileDeviceData} from "./data/MobileDeviceData"; import MobileMainView from "./views/MobileMainView/MobileMainView"; import {ISize} from "./interfaces/ISize"; import {Settings} from "./settings/Settings"; import {SizeItUpView} from "./views/SizeItUpView/SizeItUpView"; +import {PlatformModel} from "./staticModels/PlatformModel"; interface IProps { projectType: ProjectType; - mobileDeviceData: MobileDeviceData; windowSize: ISize; } -const App: React.FC = ({projectType, mobileDeviceData, windowSize}) => { +const App: React.FC = ({projectType, windowSize}) => { const selectRoute = () => { - if (!!mobileDeviceData.manufacturer && !!mobileDeviceData.os) + if (!!PlatformModel.mobileDeviceData.manufacturer && !!PlatformModel.mobileDeviceData.os) return ; if (!projectType) return ; @@ -34,7 +33,9 @@ const App: React.FC = ({projectType, mobileDeviceData, windowSize}) => { }; return ( -
+
{selectRoute()}
@@ -43,7 +44,6 @@ const App: React.FC = ({projectType, mobileDeviceData, windowSize}) => { const mapStateToProps = (state: AppState) => ({ projectType: state.editor.projectType, - mobileDeviceData: state.general.mobileDeviceData, windowSize: state.general.windowSize }); diff --git a/src/data/EditorData.ts b/src/data/EditorData.ts index 2e7b6586..b3ac3c7e 100644 --- a/src/data/EditorData.ts +++ b/src/data/EditorData.ts @@ -3,10 +3,14 @@ import {IRect} from "../interfaces/IRect"; import {ISize} from "../interfaces/ISize"; export interface EditorData { - mousePositionOnCanvas: IPoint, - canvasSize: ISize, - activeImageScale: number, - activeImageRectOnCanvas: IRect, + viewPortContentSize: ISize, + mousePositionOnViewPortContent: IPoint, activeKeyCombo: string[], event?: Event + zoom: number, + viewPortSize: ISize, + defaultRenderImageRect: IRect, + realImageSize: ISize, + viewPortContentImageRect: IRect, + absoluteViewPortContentScrollPosition: IPoint } \ No newline at end of file diff --git a/src/data/ImageButtonDropDownData.ts b/src/data/ImageButtonDropDownData.ts new file mode 100644 index 00000000..46d37f7e --- /dev/null +++ b/src/data/ImageButtonDropDownData.ts @@ -0,0 +1,5 @@ +export interface ImageButtonDropDownData { + image: string; + imageAlt: string; + onClick?: () => any; +} \ No newline at end of file diff --git a/src/data/enums/CustomCursorStyle.ts b/src/data/enums/CustomCursorStyle.ts index 7c080a6e..4cc591fa 100644 --- a/src/data/enums/CustomCursorStyle.ts +++ b/src/data/enums/CustomCursorStyle.ts @@ -4,5 +4,7 @@ export enum CustomCursorStyle { RESIZE = "RESIZE", ADD = "ADD", CANCEL = "CANCEL", - CLOSE = "CLOSE" + CLOSE = "CLOSE", + GRAB = "GRAB", + GRABBING = "GRABBING" } \ No newline at end of file diff --git a/src/data/enums/EventType.ts b/src/data/enums/EventType.ts index 3eea167a..c41dffab 100644 --- a/src/data/enums/EventType.ts +++ b/src/data/enums/EventType.ts @@ -1,7 +1,11 @@ export enum EventType { + RESIZE = "resize", MOUSE_UP = "mouseup", MOUSE_DOWN = "mousedown", MOUSE_MOVE = "mousemove", + MOUSE_WHEEL = "wheel", KEY_DOWN = "keydown", - KEY_UP = "keyup" + KEY_PRESS = "keypress", + KEY_UP = "keyup", + FOCUS = "focus" } \ No newline at end of file diff --git a/src/logic/actions/EditorActions.ts b/src/logic/actions/EditorActions.ts index 887c835f..681ef438 100644 --- a/src/logic/actions/EditorActions.ts +++ b/src/logic/actions/EditorActions.ts @@ -1,19 +1,24 @@ import {LabelType} from "../../data/enums/LabelType"; -import {EditorModel} from "../../model/EditorModel"; +import {EditorModel} from "../../staticModels/EditorModel"; import {RectRenderEngine} from "../render/RectRenderEngine"; import {PointRenderEngine} from "../render/PointRenderEngine"; import {PolygonRenderEngine} from "../render/PolygonRenderEngine"; import {IRect} from "../../interfaces/IRect"; -import {Settings} from "../../settings/Settings"; import {RectUtil} from "../../utils/RectUtil"; import {EditorData} from "../../data/EditorData"; import {CanvasUtil} from "../../utils/CanvasUtil"; -import {ISize} from "../../interfaces/ISize"; import React from "react"; import {IPoint} from "../../interfaces/IPoint"; import {DrawUtil} from "../../utils/DrawUtil"; import {PrimaryEditorRenderEngine} from "../render/PrimaryEditorRenderEngine"; import {ContextManager} from "../context/ContextManager"; +import {PointUtil} from "../../utils/PointUtil"; +import {ViewPortActions} from "./ViewPortActions"; +import {ISize} from "../../interfaces/ISize"; +import {ImageUtil} from "../../utils/ImageUtil"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; +import {ViewPortHelper} from "../helpers/ViewPortHelper"; +import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; export class EditorActions { @@ -42,7 +47,8 @@ export class EditorActions { EditorActions.mountSupportRenderingEngine(activeLabelType); }; - public static mountRenderEngines(activeLabelType: LabelType) { + public static mountRenderEnginesAndHelpers(activeLabelType: LabelType) { + EditorModel.viewPortHelper = new ViewPortHelper(); EditorModel.primaryRenderingEngine = new PrimaryEditorRenderEngine(EditorModel.canvas); EditorActions.mountSupportRenderingEngine(activeLabelType); } @@ -53,7 +59,6 @@ export class EditorActions { public static fullRender() { DrawUtil.clearCanvas(EditorModel.canvas); - EditorModel.primaryRenderingEngine.drawImage(EditorModel.image, EditorModel.imageRectOnCanvas); EditorModel.primaryRenderingEngine.render(EditorActions.getEditorData()); EditorModel.supportRenderingEngine && EditorModel.supportRenderingEngine.render(EditorActions.getEditorData()); } @@ -65,45 +70,30 @@ export class EditorActions { public static setLoadingStatus(status: boolean) { EditorModel.isLoading = status; } - public static setActiveImage(image: HTMLImageElement) { EditorModel.image = image; } + public static setViewPortActionsDisabledStatus(status: boolean) { + EditorModel.viewPortActionsDisabled = status; + } + // ================================================================================================================= // GETTERS // ================================================================================================================= - public static getImageRect(image: HTMLImageElement): IRect | null { - if (!!image) { - const canvasPaddingWidth: number = Settings.CANVAS_PADDING_WIDTH_PX; - const imageRect: IRect = { x: 0, y: 0, width: image.width, height: image.height}; - const canvasRect: IRect = { - x: canvasPaddingWidth, - y: canvasPaddingWidth, - width: EditorModel.canvas.width - 2 * canvasPaddingWidth, - height: EditorModel.canvas.height - 2 * canvasPaddingWidth - }; - return RectUtil.fitInsideRectWithRatio(canvasRect, RectUtil.getRatio(imageRect)); - } - return null; - }; - - public static getImageScale(image: HTMLImageElement): number | null { - if (!image || !EditorModel.imageRectOnCanvas) - return null; - - return image.width / EditorModel.imageRectOnCanvas.width; - } - public static getEditorData(event?: Event): EditorData { return { - mousePositionOnCanvas: EditorModel.mousePositionOnCanvas, - canvasSize: CanvasUtil.getSize(EditorModel.canvas), - activeImageScale: EditorModel.imageScale, - activeImageRectOnCanvas: EditorModel.imageRectOnCanvas, + mousePositionOnViewPortContent: EditorModel.mousePositionOnViewPortContent, + viewPortContentSize: CanvasUtil.getSize(EditorModel.canvas), activeKeyCombo: ContextManager.getActiveCombo(), - event: event + event: event, + zoom: EditorModel.zoom, + viewPortSize: EditorModel.viewPortSize, + defaultRenderImageRect: EditorModel.defaultRenderImageRect, + viewPortContentImageRect: ViewPortActions.calculateViewPortContentImageRect(), + realImageSize: ImageUtil.getSize(EditorModel.image), + absoluteViewPortContentScrollPosition: ViewPortActions.getAbsoluteScrollPosition() } } @@ -111,54 +101,42 @@ export class EditorActions { // HELPERS // ================================================================================================================= - public static calculateActiveImageCharacteristics() { - EditorModel.imageRectOnCanvas = EditorActions.getImageRect(EditorModel.image); - EditorModel.imageScale = EditorActions.getImageScale(EditorModel.image); - } - - public static resizeCanvas = (newCanvasSize: ISize) => { - if (!!newCanvasSize && !!EditorModel.canvas) { - EditorModel.canvas.width = newCanvasSize.width; - EditorModel.canvas.height = newCanvasSize.height; - } - }; - public static updateMousePositionIndicator(event: React.MouseEvent | MouseEvent) { - - if (!EditorModel.imageRectOnCanvas || !EditorModel.canvas) { + if (!EditorModel.image || !EditorModel.canvas) { EditorModel.mousePositionIndicator.style.display = "none"; EditorModel.cursor.style.display = "none"; return; } - const mousePositionOnCanvas: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, EditorModel.canvas); - const canvasRect: IRect = {x: 0, y: 0, ...CanvasUtil.getSize(EditorModel.canvas)}; - const isOverCanvas: boolean = RectUtil.isPointInside(canvasRect, mousePositionOnCanvas); - - if (!isOverCanvas) { - EditorModel.mousePositionIndicator.style.display = "none"; - EditorModel.cursor.style.display = "none"; - return; - } - - const isOverImage: boolean = RectUtil.isPointInside(EditorModel.imageRectOnCanvas, mousePositionOnCanvas); - - if (isOverImage) { - const scale = EditorModel.imageScale; - const x: number = Math.round((mousePositionOnCanvas.x - EditorModel.imageRectOnCanvas.x) * scale); - const y: number = Math.round((mousePositionOnCanvas.y - EditorModel.imageRectOnCanvas.y) * scale); - const text: string = "x: " + x + ", y: " + y; - - EditorModel.mousePositionIndicator.innerHTML = text; - EditorModel.mousePositionIndicator.style.left = (mousePositionOnCanvas.x + 15) + "px"; - EditorModel.mousePositionIndicator.style.top = (mousePositionOnCanvas.y + 15) + "px"; - EditorModel.mousePositionIndicator.style.display = "block"; + const mousePositionOverViewPortContent: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, EditorModel.canvas); + const viewPortContentScrollPosition: IPoint = ViewPortActions.getAbsoluteScrollPosition(); + const viewPortContentImageRect: IRect = ViewPortActions.calculateViewPortContentImageRect(); + const mousePositionOverViewPort: IPoint = PointUtil.subtract(mousePositionOverViewPortContent, viewPortContentScrollPosition); + const isMouseOverImage: boolean = RectUtil.isPointInside(viewPortContentImageRect, mousePositionOverViewPortContent); + const isMouseOverViewPort: boolean = RectUtil.isPointInside({x: 0, y: 0, ...EditorModel.viewPortSize}, mousePositionOverViewPort); + + if (isMouseOverViewPort && !GeneralSelector.getPreventCustomCursorStatus()) { + EditorModel.cursor.style.left = mousePositionOverViewPort.x + "px"; + EditorModel.cursor.style.top = mousePositionOverViewPort.y + "px"; + EditorModel.cursor.style.display = "block"; + + if (isMouseOverImage && ![CustomCursorStyle.GRAB, CustomCursorStyle.GRABBING].includes(GeneralSelector.getCustomCursorStyle())) { + const imageSize: ISize = ImageUtil.getSize(EditorModel.image); + const scale: number = imageSize.width / viewPortContentImageRect.width; + const mousePositionOverImage: IPoint = PointUtil.multiply( + PointUtil.subtract(mousePositionOverViewPortContent, viewPortContentImageRect), scale); + const text: string = "x: " + Math.round(mousePositionOverImage.x) + ", y: " + Math.round(mousePositionOverImage.y); + + EditorModel.mousePositionIndicator.innerHTML = text; + EditorModel.mousePositionIndicator.style.left = (mousePositionOverViewPort.x + 15) + "px"; + EditorModel.mousePositionIndicator.style.top = (mousePositionOverViewPort.y + 15) + "px"; + EditorModel.mousePositionIndicator.style.display = "block"; + } else { + EditorModel.mousePositionIndicator.style.display = "none"; + } } else { + EditorModel.cursor.style.display = "none"; EditorModel.mousePositionIndicator.style.display = "none"; } - - EditorModel.cursor.style.left = mousePositionOnCanvas.x + "px"; - EditorModel.cursor.style.top = mousePositionOnCanvas.y + "px"; - EditorModel.cursor.style.display = "block"; }; } \ No newline at end of file diff --git a/src/logic/actions/ImageActions.ts b/src/logic/actions/ImageActions.ts new file mode 100644 index 00000000..9e883866 --- /dev/null +++ b/src/logic/actions/ImageActions.ts @@ -0,0 +1,31 @@ +import {EditorSelector} from "../../store/selectors/EditorSelector"; +import {store} from "../../index"; +import {updateActiveImageIndex, updateActiveLabelId} from "../../store/editor/actionCreators"; +import {ViewPortActions} from "./ViewPortActions"; +import {EditorModel} from "../../staticModels/EditorModel"; + +export class ImageActions { + public static getPreviousImage(): void { + const currentImageIndex: number = EditorSelector.getActiveImageIndex(); + ImageActions.getImageByIndex(currentImageIndex - 1); + } + + public static getNextImage(): void { + const currentImageIndex: number = EditorSelector.getActiveImageIndex(); + ImageActions.getImageByIndex(currentImageIndex + 1); + } + + public static getImageByIndex(index: number): void { + if (EditorModel.viewPortActionsDisabled) return; + + const imageCount: number = EditorSelector.getImagesData().length; + + if (index < 0 || index > imageCount - 1) { + return; + } else { + ViewPortActions.setZoom(1); + store.dispatch(updateActiveImageIndex(index)); + store.dispatch(updateActiveLabelId(null)); + } + } +} \ No newline at end of file diff --git a/src/logic/actions/LabelActions.ts b/src/logic/actions/LabelActions.ts new file mode 100644 index 00000000..86600dc8 --- /dev/null +++ b/src/logic/actions/LabelActions.ts @@ -0,0 +1,61 @@ +import {EditorSelector} from "../../store/selectors/EditorSelector"; +import {ImageData, LabelPoint, LabelPolygon, LabelRect} from "../../store/editor/types"; +import * as _ from "lodash"; +import {store} from "../../index"; +import {updateImageDataById} from "../../store/editor/actionCreators"; +import {LabelType} from "../../data/enums/LabelType"; + +export class LabelActions { + public static deleteActiveLabel() { + const activeImageData: ImageData = EditorSelector.getActiveImageData(); + const activeLabelId: string = EditorSelector.getActiveLabelId(); + LabelActions.deleteImageLabelById(activeImageData.id, activeLabelId); + } + + public static deleteImageLabelById(imageId: string, labelId: string) { + switch (EditorSelector.getActiveLabelType()) { + case LabelType.POINT: + LabelActions.deletePointLabelById(imageId, labelId); + break; + case LabelType.RECTANGLE: + LabelActions.deleteRectLabelById(imageId, labelId); + break; + case LabelType.POLYGON: + LabelActions.deletePolygonLabelById(imageId, labelId); + break; + } + } + + public static deleteRectLabelById(imageId: string, labelRectId: string) { + const imageData: ImageData = EditorSelector.getImageDataById(imageId); + const newImageData = { + ...imageData, + labelRects: _.filter(imageData.labelRects, (currentLabel: LabelRect) => { + return currentLabel.id !== labelRectId; + }) + }; + store.dispatch(updateImageDataById(imageData.id, newImageData)); + } + + public static deletePointLabelById(imageId: string, labelPointId: string) { + const imageData: ImageData = EditorSelector.getImageDataById(imageId); + const newImageData = { + ...imageData, + labelPoints: _.filter(imageData.labelPoints, (currentLabel: LabelPoint) => { + return currentLabel.id !== labelPointId; + }) + }; + store.dispatch(updateImageDataById(imageData.id, newImageData)); + } + + public static deletePolygonLabelById(imageId: string, labelPolygonId: string) { + const imageData: ImageData = EditorSelector.getImageDataById(imageId); + const newImageData = { + ...imageData, + labelPolygons: _.filter(imageData.labelPolygons, (currentLabel: LabelPolygon) => { + return currentLabel.id !== labelPolygonId; + }) + }; + store.dispatch(updateImageDataById(imageData.id, newImageData)); + } +} \ No newline at end of file diff --git a/src/logic/actions/PopupActions.ts b/src/logic/actions/PopupActions.ts new file mode 100644 index 00000000..e4819c68 --- /dev/null +++ b/src/logic/actions/PopupActions.ts @@ -0,0 +1,10 @@ +import {ContextManager} from "../context/ContextManager"; +import {store} from "../../index"; +import {updateActivePopupType} from "../../store/general/actionCreators"; + +export class PopupActions { + public static close() { + store.dispatch(updateActivePopupType(null)); + ContextManager.restoreCtx(); + } +} \ No newline at end of file diff --git a/src/logic/actions/ViewPortActions.ts b/src/logic/actions/ViewPortActions.ts new file mode 100644 index 00000000..89c2c341 --- /dev/null +++ b/src/logic/actions/ViewPortActions.ts @@ -0,0 +1,174 @@ +import {EditorModel} from "../../staticModels/EditorModel"; +import {NumberUtil} from "../../utils/NumberUtil"; +import {ViewPointSettings} from "../../settings/ViewPointSettings"; +import {ISize} from "../../interfaces/ISize"; +import {IRect} from "../../interfaces/IRect"; +import {ImageUtil} from "../../utils/ImageUtil"; +import {RectUtil} from "../../utils/RectUtil"; +import {IPoint} from "../../interfaces/IPoint"; +import {PointUtil} from "../../utils/PointUtil"; +import {SizeUtil} from "../../utils/SizeUtil"; +import {EditorActions} from "./EditorActions"; +import {Direction} from "../../data/enums/Direction"; +import {DirectionUtil} from "../../utils/DirectionUtil"; + +export class ViewPortActions { + public static updateViewPortSize() { + if (!!EditorModel.editor) { + EditorModel.viewPortSize = { + width: EditorModel.editor.offsetWidth, + height: EditorModel.editor.offsetHeight + } + } + } + + public static updateDefaultViewPortImageRect() { + if (!!EditorModel.viewPortSize && !!EditorModel.image) { + const minMargin: IPoint = {x: ViewPointSettings.CANVAS_MIN_MARGIN_PX, y: ViewPointSettings.CANVAS_MIN_MARGIN_PX}; + const realImageRect: IRect = {x: 0, y: 0, ...ImageUtil.getSize(EditorModel.image)}; + const viewPortWithMarginRect: IRect = {x: 0, y: 0, ...EditorModel.viewPortSize}; + const viewPortWithoutMarginRect: IRect = RectUtil.expand(viewPortWithMarginRect, PointUtil.multiply(minMargin, -1)); + EditorModel.defaultRenderImageRect = RectUtil.fitInsideRectWithRatio(viewPortWithoutMarginRect, RectUtil.getRatio(realImageRect)); + } + } + + public static calculateViewPortContentSize(): ISize { + if (!!EditorModel.viewPortSize && !!EditorModel.image) { + const defaultViewPortImageRect: IRect = EditorModel.defaultRenderImageRect; + const scaledImageSize: ISize = SizeUtil.scale(EditorModel.defaultRenderImageRect, EditorModel.zoom); + return { + width: scaledImageSize.width + 2 * defaultViewPortImageRect.x, + height: scaledImageSize.height + 2 * defaultViewPortImageRect.y + } + } else { + return null; + } + } + + public static calculateViewPortContentImageRect(): IRect { + if (!!EditorModel.viewPortSize && !!EditorModel.image) { + const defaultViewPortImageRect: IRect = EditorModel.defaultRenderImageRect; + const viewPortContentSize: ISize = ViewPortActions.calculateViewPortContentSize(); + return { + ...defaultViewPortImageRect, + width: viewPortContentSize.width - 2 * defaultViewPortImageRect.x, + height: viewPortContentSize.height - 2 * defaultViewPortImageRect.y + } + } else { + return null; + } + } + + public static resizeCanvas(newCanvasSize: ISize) { + if (!!newCanvasSize && !!EditorModel.canvas) { + EditorModel.canvas.width = newCanvasSize.width; + EditorModel.canvas.height = newCanvasSize.height; + } + }; + + public static resizeViewPortContent() { + const viewPortContentSize = ViewPortActions.calculateViewPortContentSize(); + viewPortContentSize && ViewPortActions.resizeCanvas(viewPortContentSize); + } + + public static calculateAbsoluteScrollPosition(relativePosition: IPoint): IPoint { + const viewPortContentSize = ViewPortActions.calculateViewPortContentSize(); + const viewPortSize = EditorModel.viewPortSize; + return { + x: relativePosition.x * (viewPortContentSize.width - viewPortSize.width), + y: relativePosition.y * (viewPortContentSize.height - viewPortSize.height) + }; + } + + public static getRelativeScrollPosition(): IPoint { + if (!!EditorModel.viewPortScrollbars) { + const values = EditorModel.viewPortScrollbars.getValues(); + return { + x: values.left, + y: values.top + } + } else { + return null; + } + } + + public static getAbsoluteScrollPosition(): IPoint { + if (!!EditorModel.viewPortScrollbars) { + const values = EditorModel.viewPortScrollbars.getValues(); + return { + x: values.scrollLeft, + y: values.scrollTop + } + } else { + return null; + } + } + + public static setScrollPosition(position: IPoint) { + EditorModel.viewPortScrollbars.scrollLeft(position.x); + EditorModel.viewPortScrollbars.scrollTop(position.y); + } + + public static translateViewPortPosition(direction: Direction) { + if (EditorModel.viewPortActionsDisabled) return; + + const directionVector: IPoint = DirectionUtil.convertDirectionToVector(direction); + const translationVector: IPoint = PointUtil.multiply(directionVector, ViewPointSettings.TRANSLATION_STEP_PX); + const currentScrollPosition = ViewPortActions.getAbsoluteScrollPosition(); + const nextScrollPosition = PointUtil.add(currentScrollPosition, translationVector); + ViewPortActions.setScrollPosition(nextScrollPosition); + EditorActions.fullRender(); + } + + public static zoomIn() { + if (EditorModel.viewPortActionsDisabled) return; + + const currentZoom: number = EditorModel.zoom; + const currentRelativeScrollPosition: IPoint = ViewPortActions.getRelativeScrollPosition(); + const nextRelativeScrollPosition = currentZoom === 1 ? {x: 0.5, y: 0.5} : currentRelativeScrollPosition; + ViewPortActions.setZoom(currentZoom + ViewPointSettings.ZOOM_STEP); + ViewPortActions.resizeViewPortContent(); + ViewPortActions.setScrollPosition(ViewPortActions.calculateAbsoluteScrollPosition(nextRelativeScrollPosition)); + EditorActions.fullRender(); + } + + public static zoomOut() { + if (EditorModel.viewPortActionsDisabled) return; + + const currentZoom: number = EditorModel.zoom; + const currentRelativeScrollPosition: IPoint = ViewPortActions.getRelativeScrollPosition(); + ViewPortActions.setZoom(currentZoom - ViewPointSettings.ZOOM_STEP); + ViewPortActions.resizeViewPortContent(); + ViewPortActions.setScrollPosition(ViewPortActions.calculateAbsoluteScrollPosition(currentRelativeScrollPosition)); + EditorActions.fullRender(); + } + + public static setDefaultZoom() { + const currentRelativeScrollPosition: IPoint = ViewPortActions.getRelativeScrollPosition(); + ViewPortActions.setZoom(ViewPointSettings.MIN_ZOOM); + ViewPortActions.resizeViewPortContent(); + ViewPortActions.setScrollPosition(ViewPortActions.calculateAbsoluteScrollPosition(currentRelativeScrollPosition)); + EditorActions.fullRender(); + } + + public static setOneForOneZoom() { + const currentZoom: number = EditorModel.zoom; + const currentRelativeScrollPosition: IPoint = ViewPortActions.getRelativeScrollPosition(); + const nextRelativeScrollPosition = currentZoom === 1 ? {x: 0.5, y: 0.5} : currentRelativeScrollPosition; + const nextZoom: number = EditorModel.image.width / EditorModel.defaultRenderImageRect.width + ViewPortActions.setZoom(nextZoom); + ViewPortActions.resizeViewPortContent(); + ViewPortActions.setScrollPosition(ViewPortActions.calculateAbsoluteScrollPosition(nextRelativeScrollPosition)); + EditorActions.fullRender(); + } + + public static setZoom(value: number) { + const currentZoom: number = EditorModel.zoom; + const isNewValueValid: boolean = NumberUtil.isValueInRange( + value, ViewPointSettings.MIN_ZOOM, ViewPointSettings.MAX_ZOOM); + + if (isNewValueValid && value !== currentZoom) { + EditorModel.zoom = value; + } + } +} \ No newline at end of file diff --git a/src/logic/context/ContextManager.ts b/src/logic/context/ContextManager.ts index b06341c0..9c0a76c3 100644 --- a/src/logic/context/ContextManager.ts +++ b/src/logic/context/ContextManager.ts @@ -5,6 +5,8 @@ import {updateActiveContext} from "../../store/general/actionCreators"; import * as _ from "lodash"; import {EditorContext} from "./EditorContext"; import {PopupContext} from "./PopupContext"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; +import {EventType} from "../../data/enums/EventType"; export class ContextManager { private static activeCombo: string[] = []; @@ -16,11 +18,21 @@ export class ContextManager { } public static init(): void { - window.addEventListener("keydown", ContextManager.onDown); - window.addEventListener("keyup", ContextManager.onUp); + window.addEventListener(EventType.KEY_DOWN, ContextManager.onDown); + window.addEventListener(EventType.KEY_UP, ContextManager.onUp); + window.addEventListener(EventType.FOCUS, ContextManager.onFocus); } public static switchCtx(context: ContextType): void { + const activeCtx: ContextType = GeneralSelector.getActiveContext(); + + if (activeCtx !== context) { + ContextManager.contextHistory.push(activeCtx); + ContextManager.updateCtx(context); + } + } + + private static updateCtx(context: ContextType): void { store.dispatch(updateActiveContext(context)); switch (context) { case ContextType.EDITOR: @@ -34,16 +46,16 @@ export class ContextManager { } } - public static restoreContext(): void { - ContextManager.switchCtx(ContextManager.contextHistory.pop()); + public static restoreCtx(): void { + ContextManager.updateCtx(ContextManager.contextHistory.pop()); } private static onDown(event: KeyboardEvent): void { const keyCode: string = ContextManager.getKeyCodeFromEvent(event); if (!ContextManager.isInCombo(keyCode)) { ContextManager.addToCombo(keyCode); - ContextManager.execute(event); } + ContextManager.execute(event); } private static onUp(event: KeyboardEvent): void { @@ -51,6 +63,10 @@ export class ContextManager { ContextManager.removeFromCombo(keyCode); } + public static onFocus() { + ContextManager.activeCombo = []; + } + private static execute(event: KeyboardEvent): void { for (let i = 0; i < ContextManager.actions.length; i++) { const hotKey: HotKeyAction = ContextManager.actions[i]; diff --git a/src/logic/context/EditorContext.ts b/src/logic/context/EditorContext.ts index eb32cede..f416b2bf 100644 --- a/src/logic/context/EditorContext.ts +++ b/src/logic/context/EditorContext.ts @@ -1,13 +1,15 @@ import {HotKeyAction} from "../../data/HotKeyAction"; -import {EditorModel} from "../../model/EditorModel"; +import {EditorModel} from "../../staticModels/EditorModel"; import {LabelType} from "../../data/enums/LabelType"; import {EditorData} from "../../data/EditorData"; import {EditorActions} from "../actions/EditorActions"; import {PolygonRenderEngine} from "../render/PolygonRenderEngine"; -import {EditorSelector} from "../../store/selectors/EditorSelector"; -import {store} from "../../index"; -import {updateActiveImageIndex} from "../../store/editor/actionCreators"; import {BaseContext} from "./BaseContext"; +import {ImageActions} from "../actions/ImageActions"; +import {ViewPortActions} from "../actions/ViewPortActions"; +import {Direction} from "../../data/enums/Direction"; +import {PlatformUtil} from "../../utils/PlatformUtil"; +import {LabelActions} from "../actions/LabelActions"; export class EditorContext extends BaseContext { public static actions: HotKeyAction[] = [ @@ -30,29 +32,62 @@ export class EditorContext extends BaseContext { } }, { - keyCombo: ["ArrowLeft"], + keyCombo: PlatformUtil.isMac(window.navigator.userAgent) ? ["Alt", "ArrowLeft"] : ["Control", "ArrowLeft"], + action: (event: KeyboardEvent) => { + ImageActions.getPreviousImage() + } + }, + { + keyCombo: PlatformUtil.isMac(window.navigator.userAgent) ? ["Alt", "ArrowRight"] : ["Control", "ArrowRight"], + action: (event: KeyboardEvent) => { + ImageActions.getNextImage(); + } + }, + { + keyCombo: PlatformUtil.isMac(window.navigator.userAgent) ? ["Alt", "+"] : ["Control", "+"], + action: (event: KeyboardEvent) => { + ViewPortActions.zoomIn(); + } + }, + { + keyCombo: PlatformUtil.isMac(window.navigator.userAgent) ? ["Alt", "-"] : ["Control", "-"], action: (event: KeyboardEvent) => { - EditorContext.getPreviousImage(); + ViewPortActions.zoomOut(); } }, { keyCombo: ["ArrowRight"], action: (event: KeyboardEvent) => { - EditorContext.getNextImage(); + event.preventDefault(); + ViewPortActions.translateViewPortPosition(Direction.RIGHT); + } + }, + { + keyCombo: ["ArrowLeft"], + action: (event: KeyboardEvent) => { + event.preventDefault(); + ViewPortActions.translateViewPortPosition(Direction.LEFT); + } + }, + { + keyCombo: ["ArrowUp"], + action: (event: KeyboardEvent) => { + event.preventDefault(); + ViewPortActions.translateViewPortPosition(Direction.BOTTOM); + } + }, + { + keyCombo: ["ArrowDown"], + action: (event: KeyboardEvent) => { + event.preventDefault(); + ViewPortActions.translateViewPortPosition(Direction.TOP); + } + }, + { + keyCombo: PlatformUtil.isMac(window.navigator.userAgent) ? ["Backspace"] : ["Delete"], + action: (event: KeyboardEvent) => { + LabelActions.deleteActiveLabel(); } } ]; - - private static getPreviousImage(): void { - const currentImageIndex: number = EditorSelector.getActiveImageIndex(); - const previousImageIndex: number = Math.max(0, currentImageIndex - 1); - store.dispatch(updateActiveImageIndex(previousImageIndex)); - } - - private static getNextImage(): void { - const currentImageIndex: number = EditorSelector.getActiveImageIndex(); - const imageCount: number = EditorSelector.getImagesData().length; - const nextImageIndex: number = Math.min(imageCount - 1, currentImageIndex + 1); - store.dispatch(updateActiveImageIndex(nextImageIndex)); - } } \ No newline at end of file diff --git a/src/logic/context/PopupContext.ts b/src/logic/context/PopupContext.ts index 41f9d1ac..40a02205 100644 --- a/src/logic/context/PopupContext.ts +++ b/src/logic/context/PopupContext.ts @@ -1,10 +1,9 @@ import {HotKeyAction} from "../../data/HotKeyAction"; import {PopupWindowType} from "../../data/enums/PopupWindowType"; import {GeneralSelector} from "../../store/selectors/GeneralSelector"; -import {store} from "../../index"; -import {updateActivePopupType} from "../../store/general/actionCreators"; -import {ContextManager} from "./ContextManager"; import {BaseContext} from "./BaseContext"; +import {PopupActions} from "../actions/PopupActions"; +import {Settings} from "../../settings/Settings"; export class PopupContext extends BaseContext { public static actions: HotKeyAction[] = [ @@ -12,9 +11,9 @@ export class PopupContext extends BaseContext { keyCombo: ["Escape"], action: (event: KeyboardEvent) => { const popupType: PopupWindowType = GeneralSelector.getActivePopupType(); - if (popupType === PopupWindowType.LOAD_IMAGES || popupType === PopupWindowType.EXIT_PROJECT || popupType === PopupWindowType.EXPORT_LABELS) { - store.dispatch(updateActivePopupType(null)); - ContextManager.restoreContext(); + const canBeClosed: boolean = Settings.CLOSEABLE_POPUPS.includes(popupType); + if (canBeClosed) { + PopupActions.close(); } } } diff --git a/src/logic/helpers/ViewPortHelper.ts b/src/logic/helpers/ViewPortHelper.ts new file mode 100644 index 00000000..024b0fd4 --- /dev/null +++ b/src/logic/helpers/ViewPortHelper.ts @@ -0,0 +1,63 @@ +import {EditorData} from "../../data/EditorData"; +import {MouseEventUtil} from "../../utils/MouseEventUtil"; +import {EventType} from "../../data/enums/EventType"; +import {store} from "../../index"; +import {updateCustomCursorStyle} from "../../store/general/actionCreators"; +import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; +import {EditorModel} from "../../staticModels/EditorModel"; +import {IPoint} from "../../interfaces/IPoint"; +import {PointUtil} from "../../utils/PointUtil"; +import {ViewPortActions} from "../actions/ViewPortActions"; + +export class ViewPortHelper { + private startScrollPosition: IPoint; + private mouseStartPosition: IPoint; + + public update(data: EditorData): void { + if (!!data.event) { + switch (MouseEventUtil.getEventType(data.event)) { + case EventType.MOUSE_MOVE: + this.mouseMoveHandler(data); + break; + case EventType.MOUSE_UP: + this.mouseUpHandler(data); + break; + case EventType.MOUSE_DOWN: + this.mouseDownHandler(data); + break; + default: + break; + } + } + } + + private mouseDownHandler(data: EditorData) { + const event = data.event as MouseEvent; + this.startScrollPosition = data.absoluteViewPortContentScrollPosition; + this.mouseStartPosition = {x: event.screenX, y: event.screenY}; + + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.GRABBING)); + EditorModel.canvas.style.cursor = "none"; + } + + private mouseUpHandler(data: EditorData) { + this.startScrollPosition = null; + this.mouseStartPosition = null; + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.GRAB)); + EditorModel.canvas.style.cursor = "none"; + } + + private mouseMoveHandler(data: EditorData) { + if (!!this.startScrollPosition && !!this.mouseStartPosition) { + const event = data.event as MouseEvent; + const currentMousePosition: IPoint = {x: event.screenX, y: event.screenY}; + const mousePositionDelta: IPoint = PointUtil.subtract(currentMousePosition, this.mouseStartPosition); + const nextScrollPosition = PointUtil.subtract(this.startScrollPosition, mousePositionDelta); + ViewPortActions.setScrollPosition(nextScrollPosition); + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.GRABBING)); + } else { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.GRAB)); + } + EditorModel.canvas.style.cursor = "none"; + } +} \ No newline at end of file diff --git a/src/logic/initializer/AppInitializer.ts b/src/logic/initializer/AppInitializer.ts index 0c148694..d89ff00a 100644 --- a/src/logic/initializer/AppInitializer.ts +++ b/src/logic/initializer/AppInitializer.ts @@ -1,13 +1,18 @@ -import {updateMobileDeviceData, updateWindowSize} from "../../store/general/actionCreators"; +import {updateWindowSize} from "../../store/general/actionCreators"; import {ContextManager} from "../context/ContextManager"; -import MobileDetect from 'mobile-detect' import {store} from "../../index"; +import {PlatformUtil} from "../../utils/PlatformUtil"; +import {PlatformModel} from "../../staticModels/PlatformModel"; +import {EventType} from "../../data/enums/EventType"; export class AppInitializer { public static inti():void { AppInitializer.handleResize(); AppInitializer.detectDeviceParams(); - window.addEventListener("resize", AppInitializer.handleResize); + window.addEventListener(EventType.RESIZE, AppInitializer.handleResize); + window.addEventListener(EventType.MOUSE_WHEEL, AppInitializer.disableGenericScrollZoom); + window.addEventListener(EventType.KEY_DOWN, AppInitializer.disableGenericKeyBordZoom); + window.addEventListener(EventType.KEY_PRESS, AppInitializer.disableGenericKeyBordZoom); ContextManager.init(); } @@ -18,12 +23,29 @@ export class AppInitializer { })); }; + public static disableGenericKeyBordZoom = (event: KeyboardEvent) => { + if (PlatformModel.isMac && event.metaKey) { + event.preventDefault(); + } + + if (["=", "+", "-"].includes(event.key)) { + if (event.ctrlKey || (PlatformModel.isMac && event.metaKey)) { + event.preventDefault(); + } + } + }; + + private static disableGenericScrollZoom = (event: MouseEvent) => { + if (event.ctrlKey || (PlatformModel.isMac && event.metaKey)) { + event.preventDefault(); + } + }; + private static detectDeviceParams = () => { - const mobileDetect = new MobileDetect(window.navigator.userAgent); - store.dispatch(updateMobileDeviceData({ - manufacturer: mobileDetect.mobile(), - browser: mobileDetect.userAgent(), - os: mobileDetect.os() - })) + const userAgent: string = window.navigator.userAgent; + PlatformModel.mobileDeviceData = PlatformUtil.getMobileDeviceData(userAgent); + PlatformModel.isMac = PlatformUtil.isMac(userAgent); + PlatformModel.isSafari = PlatformUtil.isSafari(userAgent); + PlatformModel.isFirefox = PlatformUtil.isFirefox(userAgent); }; } \ No newline at end of file diff --git a/src/logic/render/PointRenderEngine.ts b/src/logic/render/PointRenderEngine.ts index 5d49748d..18077aa1 100644 --- a/src/logic/render/PointRenderEngine.ts +++ b/src/logic/render/PointRenderEngine.ts @@ -20,6 +20,9 @@ import {EditorData} from "../../data/EditorData"; import {BaseRenderEngine} from "./BaseRenderEngine"; import {RenderEngineUtil} from "../../utils/RenderEngineUtil"; import {LabelType} from "../../data/enums/LabelType"; +import {EditorActions} from "../actions/EditorActions"; +import {EditorModel} from "../../staticModels/EditorModel"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; export class PointRenderEngine extends BaseRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); @@ -28,8 +31,6 @@ export class PointRenderEngine extends BaseRenderEngine { // STATE // ================================================================================================================= - private transformInProgress: boolean = false; - public constructor(canvas: HTMLCanvasElement) { super(canvas); this.labelType = LabelType.POINT; @@ -44,22 +45,22 @@ export class PointRenderEngine extends BaseRenderEngine { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { - const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnCanvas, data); + const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnViewPortContent, data); if (!!labelPoint) { - const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToCanvas(labelPoint.point, data); + const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToViewPortContent(labelPoint.point, data); const pointBetweenPixels = RenderEngineUtil.setPointBetweenPixels(pointOnCanvas); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointBetweenPixels, this.config.anchorHoverSize); - if (RectUtil.isPointInside(handleRect, data.mousePositionOnCanvas)) { + if (RectUtil.isPointInside(handleRect, data.mousePositionOnViewPortContent)) { store.dispatch(updateActiveLabelId(labelPoint.id)); - this.transformInProgress = true; + EditorActions.setViewPortActionsDisabledStatus(true); return; } else { store.dispatch(updateActiveLabelId(null)); - const pointOnImage: IPoint = RenderEngineUtil.transferPointFromCanvasToImage(data.mousePositionOnCanvas, data); + const pointOnImage: IPoint = RenderEngineUtil.transferPointFromViewPortContentToImage(data.mousePositionOnViewPortContent, data); this.addPointLabel(pointOnImage); } } else if (isMouseOverImage) { - const pointOnImage: IPoint = RenderEngineUtil.transferPointFromCanvasToImage(data.mousePositionOnCanvas, data); + const pointOnImage: IPoint = RenderEngineUtil.transferPointFromViewPortContentToImage(data.mousePositionOnViewPortContent, data); this.addPointLabel(pointOnImage); } } @@ -68,8 +69,8 @@ export class PointRenderEngine extends BaseRenderEngine { public mouseUpHandler(data: EditorData): void { if (this.isInProgress()) { const activeLabelPoint: LabelPoint = EditorSelector.getActivePointLabel(); - const pointSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); - const pointOnImage: IPoint = RenderEngineUtil.transferPointFromCanvasToImage(pointSnapped, data); + const pointSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + const pointOnImage: IPoint = RenderEngineUtil.transferPointFromViewPortContentToImage(pointSnapped, data); const imageData = EditorSelector.getActiveImageData(); imageData.labelPoints = imageData.labelPoints.map((labelPoint: LabelPoint) => { @@ -83,13 +84,13 @@ export class PointRenderEngine extends BaseRenderEngine { }); store.dispatch(updateImageDataById(imageData.id, imageData)); } - this.transformInProgress = false; + EditorActions.setViewPortActionsDisabledStatus(false); } public mouseMoveHandler(data: EditorData): void { const isOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); if (isOverImage) { - const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnCanvas, data); + const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnViewPortContent, data); if (!!labelPoint) { if (EditorSelector.getHighlightedLabelId() !== labelPoint.id) { store.dispatch(updateHighlightedLabelId(labelPoint.id)) @@ -114,7 +115,7 @@ export class PointRenderEngine extends BaseRenderEngine { imageData.labelPoints.forEach((labelPoint: LabelPoint) => { if (labelPoint.id === activeLabelId) { if (this.isInProgress()) { - const pointSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + const pointSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); const pointBetweenPixels: IPoint = RenderEngineUtil.setPointBetweenPixels(pointSnapped); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointBetweenPixels, this.config.anchorSize); DrawUtil.drawRectWithFill(this.canvas, handleRect, this.config.activeAnchorColor); @@ -130,7 +131,7 @@ export class PointRenderEngine extends BaseRenderEngine { } private renderPoint(labelPoint: LabelPoint, isActive: boolean, data: EditorData) { - const pointOnImage: IPoint = RenderEngineUtil.transferPointFromImageToCanvas(labelPoint.point, data); + const pointOnImage: IPoint = RenderEngineUtil.transferPointFromImageToViewPortContent(labelPoint.point, data); const pointBetweenPixels = RenderEngineUtil.setPointBetweenPixels(pointOnImage); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointBetweenPixels, this.config.anchorSize); const handleColor: string = isActive ? this.config.activeAnchorColor : this.config.inactiveAnchorColor; @@ -138,13 +139,13 @@ export class PointRenderEngine extends BaseRenderEngine { } private updateCursorStyle(data: EditorData) { - if (!!this.canvas && !!data.mousePositionOnCanvas) { - const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnCanvas, data); + if (!!this.canvas && !!data.mousePositionOnViewPortContent && !GeneralSelector.getImageDragModeStatus()) { + const labelPoint: LabelPoint = this.getLabelPointUnderMouse(data.mousePositionOnViewPortContent, data); if (!!labelPoint) { - const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToCanvas(labelPoint.point, data); + const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToViewPortContent(labelPoint.point, data); const pointBetweenPixels = RenderEngineUtil.setPointBetweenPixels(pointOnCanvas); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointBetweenPixels, this.config.anchorHoverSize); - if (RectUtil.isPointInside(handleRect, data.mousePositionOnCanvas)) { + if (RectUtil.isPointInside(handleRect, data.mousePositionOnViewPortContent)) { store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MOVE)); return; } @@ -153,7 +154,7 @@ export class PointRenderEngine extends BaseRenderEngine { return; } - if (RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, data.mousePositionOnCanvas)) { + if (RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, data.mousePositionOnViewPortContent)) { RenderEngineUtil.wrapDefaultCursorStyleInCancel(data); this.canvas.style.cursor = "none"; } else { @@ -167,13 +168,13 @@ export class PointRenderEngine extends BaseRenderEngine { // ================================================================================================================= public isInProgress(): boolean { - return this.transformInProgress; + return EditorModel.viewPortActionsDisabled; } private getLabelPointUnderMouse(mousePosition: IPoint, data: EditorData): LabelPoint { const labelPoints: LabelPoint[] = EditorSelector.getActiveImageData().labelPoints; for (let i = 0; i < labelPoints.length; i++) { - const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToCanvas(labelPoints[i].point, data); + const pointOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToViewPortContent(labelPoints[i].point, data); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointOnCanvas, this.config.anchorHoverSize); if (RectUtil.isPointInside(handleRect, mousePosition)) { return labelPoints[i]; diff --git a/src/logic/render/PolygonRenderEngine.ts b/src/logic/render/PolygonRenderEngine.ts index 3b76571b..b7223794 100644 --- a/src/logic/render/PolygonRenderEngine.ts +++ b/src/logic/render/PolygonRenderEngine.ts @@ -23,6 +23,8 @@ import {MouseEventUtil} from "../../utils/MouseEventUtil"; import {EventType} from "../../data/enums/EventType"; import {RenderEngineUtil} from "../../utils/RenderEngineUtil"; import {LabelType} from "../../data/enums/LabelType"; +import {EditorActions} from "../actions/EditorActions"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; export class PolygonRenderEngine extends BaseRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); @@ -67,7 +69,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { if (this.isCreationInProgress()) { - const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnCanvas, this.activePath[0]); + const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.activePath[0]); if (isMouseOverStartAnchor) { this.addLabelAndFinishCreation(data); } else { @@ -79,8 +81,8 @@ export class PolygonRenderEngine extends BaseRenderEngine { const anchorIndex: number = polygonUnderMouse.vertices.reduce( (indexUnderMouse: number, anchor: IPoint, index: number) => { if (indexUnderMouse === null) { - const anchorOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToCanvas(anchor, data); - if (this.isMouseOverAnchor(data.mousePositionOnCanvas, anchorOnCanvas)) { + const anchorOnCanvas: IPoint = RenderEngineUtil.transferPointFromImageToViewPortContent(anchor, data); + if (this.isMouseOverAnchor(data.mousePositionOnViewPortContent, anchorOnCanvas)) { return index; } } @@ -90,12 +92,10 @@ export class PolygonRenderEngine extends BaseRenderEngine { if (anchorIndex !== null) { this.startExistingLabelResize(data, polygonUnderMouse.id, anchorIndex); } else { - const isMouseOverNewAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnCanvas, this.suggestedAnchorPositionOnCanvas); + store.dispatch(updateActiveLabelId(polygonUnderMouse.id)); + const isMouseOverNewAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.suggestedAnchorPositionOnCanvas); if (isMouseOverNewAnchor) { - store.dispatch(updateActiveLabelId(polygonUnderMouse.id)); this.addSuggestedAnchorToPolygonLabel(data); - } else { - this.updateActivelyCreatedLabel(data); } } } else { @@ -111,7 +111,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { } public mouseMoveHandler(data: EditorData): void { - if (!!data.activeImageRectOnCanvas && !!data.mousePositionOnCanvas) { + if (!!data.viewPortContentImageRect && !!data.mousePositionOnViewPortContent) { const isOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); if (isOverImage && !this.isCreationInProgress()) { const labelPolygon: LabelPolygon = this.getPolygonUnderMouse(data); @@ -119,11 +119,11 @@ export class PolygonRenderEngine extends BaseRenderEngine { if (EditorSelector.getHighlightedLabelId() !== labelPolygon.id) { store.dispatch(updateHighlightedLabelId(labelPolygon.id)) } - const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToCanvas(labelPolygon.vertices, data); + const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToViewPortContent(labelPolygon.vertices, data); const linesOnCanvas: ILine[] = this.mapPointsToLines(pathOnCanvas.concat(pathOnCanvas[0])); for (let j = 0; j < linesOnCanvas.length; j++) { - if (this.isMouseOverLine(data.mousePositionOnCanvas, linesOnCanvas[j])) { + if (this.isMouseOverLine(data.mousePositionOnViewPortContent, linesOnCanvas[j])) { this.suggestedAnchorPositionOnCanvas = LineUtil.getCenter(linesOnCanvas[j]); this.suggestedAnchorIndexInPolygon = j + 1; break; @@ -155,18 +155,18 @@ export class PolygonRenderEngine extends BaseRenderEngine { } private updateCursorStyle(data: EditorData) { - if (!!this.canvas && !!data.mousePositionOnCanvas) { + if (!!this.canvas && !!data.mousePositionOnViewPortContent && !GeneralSelector.getImageDragModeStatus()) { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { if (this.isCreationInProgress()) { - const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnCanvas, this.activePath[0]); + const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.activePath[0]); if (isMouseOverStartAnchor && this.activePath.length > 2) store.dispatch(updateCustomCursorStyle(CustomCursorStyle.CLOSE)); else store.dispatch(updateCustomCursorStyle(CustomCursorStyle.DEFAULT)); } else { const anchorUnderMouse: IPoint = this.getAnchorUnderMouse(data); - const isMouseOverNewAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnCanvas, this.suggestedAnchorPositionOnCanvas); + const isMouseOverNewAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.suggestedAnchorPositionOnCanvas); if (!!isMouseOverNewAnchor) { store.dispatch(updateCustomCursorStyle(CustomCursorStyle.ADD)); } else if (this.isResizeInProgress()) { @@ -186,7 +186,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { private drawActivelyCreatedLabel(data: EditorData) { const standardizedPoints: IPoint[] = this.activePath.map((point: IPoint) => RenderEngineUtil.setPointBetweenPixels(point)); - const path = standardizedPoints.concat(data.mousePositionOnCanvas); + const path = standardizedPoints.concat(data.mousePositionOnViewPortContent); const lines: ILine[] = this.mapPointsToLines(path); DrawUtil.drawPolygonWithFill(this.canvas, path, DrawUtil.hexToRGB(this.config.lineActiveColor, 0.2)); @@ -201,9 +201,9 @@ export class PolygonRenderEngine extends BaseRenderEngine { private drawActivelyResizeLabel(data: EditorData) { const activeLabelPolygon: LabelPolygon = EditorSelector.getActivePolygonLabel(); if (!!activeLabelPolygon && this.isResizeInProgress()) { - const snappedMousePosition: IPoint = RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + const snappedMousePosition: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); const polygonOnCanvas: IPoint[] = activeLabelPolygon.vertices.map((point: IPoint, index: number) => { - return index === this.resizeAnchorIndex ? snappedMousePosition : RenderEngineUtil.transferPointFromImageToCanvas(point, data); + return index === this.resizeAnchorIndex ? snappedMousePosition : RenderEngineUtil.transferPointFromImageToViewPortContent(point, data); }); this.drawPolygon(polygonOnCanvas, true); } @@ -215,7 +215,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { const imageData: ImageData = EditorSelector.getActiveImageData(); imageData.labelPolygons.forEach((labelPolygon: LabelPolygon) => { const isActive: boolean = labelPolygon.id === activeLabelId || labelPolygon.id === highlightedLabelId; - const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToCanvas(labelPolygon.vertices, data); + const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToViewPortContent(labelPolygon.vertices, data); if (!(labelPolygon.id === activeLabelId && this.isResizeInProgress())) { this.drawPolygon(pathOnCanvas, isActive); } @@ -240,7 +240,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { if (this.suggestedAnchorPositionOnCanvas) { const suggestedAnchorRect: IRect = RectUtil .getRectWithCenterAndSize(this.suggestedAnchorPositionOnCanvas, this.config.suggestedAnchorDetectionSize); - const isMouseOverSuggestedAnchor: boolean = RectUtil.isPointInside(suggestedAnchorRect, data.mousePositionOnCanvas); + const isMouseOverSuggestedAnchor: boolean = RectUtil.isPointInside(suggestedAnchorRect, data.mousePositionOnViewPortContent); if (isMouseOverSuggestedAnchor) { const handleRect = RectUtil.getRectWithCenterAndSize(this.suggestedAnchorPositionOnCanvas, this.config.anchorSize); @@ -255,12 +255,13 @@ export class PolygonRenderEngine extends BaseRenderEngine { private updateActivelyCreatedLabel(data: EditorData) { if (this.isCreationInProgress()) { - const mousePositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + const mousePositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); this.activePath.push(mousePositionSnapped); } else { - const isMouseOverImage: boolean = RectUtil.isPointInside(data.activeImageRectOnCanvas, data.mousePositionOnCanvas); + const isMouseOverImage: boolean = RectUtil.isPointInside(data.viewPortContentImageRect, data.mousePositionOnViewPortContent); if (isMouseOverImage) { - this.activePath.push(data.mousePositionOnCanvas); + EditorActions.setViewPortActionsDisabledStatus(true); + this.activePath.push(data.mousePositionOnViewPortContent); store.dispatch(updateActiveLabelId(null)); } } @@ -268,15 +269,17 @@ export class PolygonRenderEngine extends BaseRenderEngine { public cancelLabelCreation() { this.activePath = []; + EditorActions.setViewPortActionsDisabledStatus(false); } private finishLabelCreation() { this.activePath = []; + EditorActions.setViewPortActionsDisabledStatus(false); } public addLabelAndFinishCreation(data: EditorData) { if (this.isCreationInProgress() && this.activePath.length > 2) { - const polygonOnImage: IPoint[] = RenderEngineUtil.transferPolygonFromCanvasToImage(this.activePath, data); + const polygonOnImage: IPoint[] = RenderEngineUtil.transferPolygonFromViewPortContentToImage(this.activePath, data); this.addPolygonLabel(polygonOnImage); this.finishLabelCreation(); } @@ -303,11 +306,13 @@ export class PolygonRenderEngine extends BaseRenderEngine { private startExistingLabelResize(data: EditorData, labelId: string, anchorIndex: number) { store.dispatch(updateActiveLabelId(labelId)); this.resizeAnchorIndex = anchorIndex; + EditorActions.setViewPortActionsDisabledStatus(true); } private endExistingLabelResize(data: EditorData) { this.applyResizeToPolygonLabel(data); this.resizeAnchorIndex = null; + EditorActions.setViewPortActionsDisabledStatus(false); } private applyResizeToPolygonLabel(data: EditorData) { @@ -324,8 +329,8 @@ export class PolygonRenderEngine extends BaseRenderEngine { return value; } else { const snappedMousePosition: IPoint = - RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); - return RenderEngineUtil.transferPointFromCanvasToImage(snappedMousePosition, data); + RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + return RenderEngineUtil.transferPointFromViewPortContentToImage(snappedMousePosition, data); } }) } @@ -348,7 +353,7 @@ export class PolygonRenderEngine extends BaseRenderEngine { const imageData: ImageData = EditorSelector.getActiveImageData(); const activeLabel: LabelPolygon = EditorSelector.getActivePolygonLabel(); const newAnchorPositionOnImage: IPoint = - RenderEngineUtil.transferPointFromCanvasToImage(this.suggestedAnchorPositionOnCanvas, data); + RenderEngineUtil.transferPointFromViewPortContentToImage(this.suggestedAnchorPositionOnCanvas, data); const insert = (arr, index, newItem) => [...arr.slice(0, index), newItem, ...arr.slice(index)]; const newImageData: ImageData = { @@ -426,15 +431,15 @@ export class PolygonRenderEngine extends BaseRenderEngine { private getPolygonUnderMouse(data: EditorData): LabelPolygon { const labelPolygons: LabelPolygon[] = EditorSelector.getActiveImageData().labelPolygons; for (let i = 0; i < labelPolygons.length; i++) { - const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToCanvas(labelPolygons[i].vertices, data); + const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToViewPortContent(labelPolygons[i].vertices, data); const linesOnCanvas: ILine[] = this.mapPointsToLines(pathOnCanvas.concat(pathOnCanvas[0])); for (let j = 0; j < linesOnCanvas.length; j++) { - if (this.isMouseOverLine(data.mousePositionOnCanvas, linesOnCanvas[j])) + if (this.isMouseOverLine(data.mousePositionOnViewPortContent, linesOnCanvas[j])) return labelPolygons[i]; } for (let j = 0; j < pathOnCanvas.length; j ++) { - if (this.isMouseOverAnchor(data.mousePositionOnCanvas, pathOnCanvas[j])) + if (this.isMouseOverAnchor(data.mousePositionOnViewPortContent, pathOnCanvas[j])) return labelPolygons[i]; } } @@ -444,9 +449,9 @@ export class PolygonRenderEngine extends BaseRenderEngine { private getAnchorUnderMouse(data: EditorData): IPoint { const labelPolygons: LabelPolygon[] = EditorSelector.getActiveImageData().labelPolygons; for (let i = 0; i < labelPolygons.length; i++) { - const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToCanvas(labelPolygons[i].vertices, data); + const pathOnCanvas: IPoint[] = RenderEngineUtil.transferPolygonFromImageToViewPortContent(labelPolygons[i].vertices, data); for (let j = 0; j < pathOnCanvas.length; j ++) { - if (this.isMouseOverAnchor(data.mousePositionOnCanvas, pathOnCanvas[j])) + if (this.isMouseOverAnchor(data.mousePositionOnViewPortContent, pathOnCanvas[j])) return pathOnCanvas[j]; } } diff --git a/src/logic/render/PrimaryEditorRenderEngine.ts b/src/logic/render/PrimaryEditorRenderEngine.ts index 1323faa1..45383dba 100644 --- a/src/logic/render/PrimaryEditorRenderEngine.ts +++ b/src/logic/render/PrimaryEditorRenderEngine.ts @@ -1,6 +1,8 @@ import {IRect} from "../../interfaces/IRect"; import {BaseRenderEngine} from "./BaseRenderEngine"; import {EditorData} from "../../data/EditorData"; +import {EditorModel} from "../../staticModels/EditorModel"; +import {ViewPortActions} from "../actions/ViewPortActions"; export class PrimaryEditorRenderEngine extends BaseRenderEngine { @@ -20,7 +22,9 @@ export class PrimaryEditorRenderEngine extends BaseRenderEngine { // RENDERING // ================================================================================================================= - public render(data: EditorData): void {} + public render(data: EditorData): void { + EditorModel.primaryRenderingEngine.drawImage(EditorModel.image, ViewPortActions.calculateViewPortContentImageRect()); + } public drawImage(image: HTMLImageElement, imageRect: IRect) { if (!!image && !!this.canvas) { diff --git a/src/logic/render/RectRenderEngine.ts b/src/logic/render/RectRenderEngine.ts index 497a4c16..27457f0f 100644 --- a/src/logic/render/RectRenderEngine.ts +++ b/src/logic/render/RectRenderEngine.ts @@ -21,6 +21,8 @@ import {EditorData} from "../../data/EditorData"; import {BaseRenderEngine} from "./BaseRenderEngine"; import {RenderEngineUtil} from "../../utils/RenderEngineUtil"; import {LabelType} from "../../data/enums/LabelType"; +import {EditorActions} from "../actions/EditorActions"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; export class RectRenderEngine extends BaseRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); @@ -45,10 +47,10 @@ export class RectRenderEngine extends BaseRenderEngine { const isMouseOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { - const rectUnderMouse: LabelRect = this.getRectUnderMouse(data.activeImageScale, data.activeImageRectOnCanvas, data.mousePositionOnCanvas); + const rectUnderMouse: LabelRect = this.getRectUnderMouse(data); if (!!rectUnderMouse) { - const rect: IRect = this.calculateRectRelativeToActiveImage(rectUnderMouse.rect, data.activeImageScale); - const anchorUnderMouse: RectAnchor = this.getAnchorUnderMouseByRect(rect, data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + const rect: IRect = this.calculateRectRelativeToActiveImage(rectUnderMouse.rect, data); + const anchorUnderMouse: RectAnchor = this.getAnchorUnderMouseByRect(rect, data.mousePositionOnViewPortContent, data.viewPortContentImageRect); if (!!anchorUnderMouse) { store.dispatch(updateActiveLabelId(rectUnderMouse.id)); this.startRectResize(anchorUnderMouse); @@ -56,18 +58,19 @@ export class RectRenderEngine extends BaseRenderEngine { if (!!EditorSelector.getHighlightedLabelId()) store.dispatch(updateActiveLabelId(EditorSelector.getHighlightedLabelId())); else - this.startRectCreation(data.mousePositionOnCanvas); + this.startRectCreation(data.mousePositionOnViewPortContent); } } else if (isMouseOverImage) { - this.startRectCreation(data.mousePositionOnCanvas); + this.startRectCreation(data.mousePositionOnViewPortContent); } } }; public mouseUpHandler = (data: EditorData) => { - if (!!data.activeImageRectOnCanvas) { - const mousePositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + if (!!data.viewPortContentImageRect) { + const mousePositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + const activeLabelRect: LabelRect = EditorSelector.getActiveRectLabel(); if (!!this.startCreateRectPoint && !PointUtil.equals(this.startCreateRectPoint, mousePositionSnapped)) { @@ -76,23 +79,18 @@ export class RectRenderEngine extends BaseRenderEngine { const maxX: number = Math.max(this.startCreateRectPoint.x, mousePositionSnapped.x); const maxY: number = Math.max(this.startCreateRectPoint.y, mousePositionSnapped.y); - const rect: IRect = { - x: (minX - data.activeImageRectOnCanvas.x) * data.activeImageScale, - y: (minY - data.activeImageRectOnCanvas.y) * data.activeImageScale, - width: (maxX - minX) * data.activeImageScale, - height: (maxY - minY) * data.activeImageScale - }; - this.addRectLabel(rect); + const rect = {x: minX, y: minY, width: maxX - minX, height: maxY - minY}; + this.addRectLabel(RenderEngineUtil.transferRectFromImageToViewPortContent(rect, data)); } - if (!!this.startResizeRectAnchor) { - const activeLabelRect: LabelRect = EditorSelector.getActiveRectLabel(); - const rect: IRect = this.calculateRectRelativeToActiveImage(activeLabelRect.rect, data.activeImageScale); + if (!!this.startResizeRectAnchor && !!activeLabelRect) { + const rect: IRect = this.calculateRectRelativeToActiveImage(activeLabelRect.rect, data); const startAnchorPosition: IPoint = PointUtil.add(this.startResizeRectAnchor.position, - data.activeImageRectOnCanvas); + data.viewPortContentImageRect); const delta: IPoint = PointUtil.subtract(mousePositionSnapped, startAnchorPosition); const resizeRect: IRect = RectUtil.resizeRect(rect, this.startResizeRectAnchor.type, delta); - const scaledRect: IRect = RectUtil.scaleRect(resizeRect, data.activeImageScale); + const scale: number = RenderEngineUtil.calculateImageScale(data); + const scaledRect: IRect = RectUtil.scaleRect(resizeRect, scale); const imageData = EditorSelector.getActiveImageData(); imageData.labelRects = imageData.labelRects.map((labelRect: LabelRect) => { @@ -111,11 +109,11 @@ export class RectRenderEngine extends BaseRenderEngine { }; public mouseMoveHandler = (data: EditorData) => { - if (!!data.activeImageRectOnCanvas && !!data.mousePositionOnCanvas) { + if (!!data.viewPortContentImageRect && !!data.mousePositionOnViewPortContent) { const isOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); if (isOverImage && !this.startResizeRectAnchor) { - const labelRect: LabelRect = this.getRectUnderMouse(data.activeImageScale, data.activeImageRectOnCanvas, data.mousePositionOnCanvas); - if (!!labelRect) { + const labelRect: LabelRect = this.getRectUnderMouse(data); + if (!!labelRect && !this.isInProgress()) { if (EditorSelector.getHighlightedLabelId() !== labelRect.id) { store.dispatch(updateHighlightedLabelId(labelRect.id)) } @@ -138,9 +136,9 @@ export class RectRenderEngine extends BaseRenderEngine { if (imageData) { imageData.labelRects.forEach((labelRect: LabelRect) => { - labelRect.id === activeLabelId ? this.drawActiveRect(labelRect, data.mousePositionOnCanvas, data.activeImageRectOnCanvas, data.activeImageScale) : this.drawInactiveRect(labelRect, data); + labelRect.id === activeLabelId ? this.drawActiveRect(labelRect, data) : this.drawInactiveRect(labelRect, data); }); - this.drawCurrentlyCreatedRect(data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + this.drawCurrentlyCreatedRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); this.updateCursorStyle(data); } } @@ -160,20 +158,20 @@ export class RectRenderEngine extends BaseRenderEngine { } private drawInactiveRect(labelRect: LabelRect, data: EditorData) { - const rectOnImage: IRect = RenderEngineUtil.transferRectFromCanvasToImage(labelRect.rect, data); + const rectOnImage: IRect = RenderEngineUtil.transferRectFromViewPortContentToImage(labelRect.rect, data); const highlightedLabelId: string = EditorSelector.getHighlightedLabelId(); this.renderRect(rectOnImage, labelRect.id === highlightedLabelId); } - private drawActiveRect(labelRect: LabelRect, mousePosition: IPoint, imageRect: IRect, scale: number) { - let rect: IRect = this.calculateRectRelativeToActiveImage(labelRect.rect, scale); + private drawActiveRect(labelRect: LabelRect, data: EditorData) { + let rect: IRect = this.calculateRectRelativeToActiveImage(labelRect.rect, data); if (!!this.startResizeRectAnchor) { - const startAnchorPosition: IPoint = PointUtil.add(this.startResizeRectAnchor.position, imageRect); - const endAnchorPositionSnapped: IPoint = RectUtil.snapPointToRect(mousePosition, imageRect); + const startAnchorPosition: IPoint = PointUtil.add(this.startResizeRectAnchor.position, data.viewPortContentImageRect); + const endAnchorPositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); const delta = PointUtil.subtract(endAnchorPositionSnapped, startAnchorPosition); rect = RectUtil.resizeRect(rect, this.startResizeRectAnchor.type, delta); } - const rectOnImage: IRect = RectUtil.translate(rect, imageRect); + const rectOnImage: IRect = RectUtil.translate(rect, data.viewPortContentImageRect); this.renderRect(rectOnImage, true); } @@ -192,14 +190,14 @@ export class RectRenderEngine extends BaseRenderEngine { } private updateCursorStyle(data: EditorData) { - if (!!this.canvas && !!data.mousePositionOnCanvas) { - const rectAnchorUnderMouse: RectAnchor = this.getAnchorUnderMouse(data.activeImageScale, data.mousePositionOnCanvas, data.activeImageRectOnCanvas); + if (!!this.canvas && !!data.mousePositionOnViewPortContent && !GeneralSelector.getImageDragModeStatus()) { + const rectAnchorUnderMouse: RectAnchor = this.getAnchorUnderMouse(data); if (!!rectAnchorUnderMouse || !!this.startResizeRectAnchor) { store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MOVE)); return; } if (RenderEngineUtil.isMouseOverCanvas(data)) { - if (!RectUtil.isPointInside(data.activeImageRectOnCanvas, data.mousePositionOnCanvas) && !!this.startCreateRectPoint) + if (!RenderEngineUtil.isMouseOverImage(data) && !!this.startCreateRectPoint) store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MOVE)); else RenderEngineUtil.wrapDefaultCursorStyleInCancel(data); @@ -218,7 +216,8 @@ export class RectRenderEngine extends BaseRenderEngine { return !!this.startCreateRectPoint || !!this.startResizeRectAnchor; } - private calculateRectRelativeToActiveImage(rect: IRect, scale: number):IRect { + private calculateRectRelativeToActiveImage(rect: IRect, data: EditorData):IRect { + const scale: number = RenderEngineUtil.calculateImageScale(data); return RectUtil.scaleRect(rect, 1/scale); } @@ -236,24 +235,24 @@ export class RectRenderEngine extends BaseRenderEngine { store.dispatch(updateActiveLabelId(labelRect.id)); }; - private getRectUnderMouse(scale: number, imageRect: IRect, mousePosition: IPoint): LabelRect { + private getRectUnderMouse(data: EditorData): LabelRect { const activeRectLabel: LabelRect = EditorSelector.getActiveRectLabel(); - if (!!activeRectLabel && this.isMouseOverRectEdges(activeRectLabel.rect, scale, imageRect, mousePosition)) { + if (!!activeRectLabel && this.isMouseOverRectEdges(activeRectLabel.rect, data)) { return activeRectLabel; } const labelRects: LabelRect[] = EditorSelector.getActiveImageData().labelRects; for (let i = 0; i < labelRects.length; i++) { - if (this.isMouseOverRectEdges(labelRects[i].rect, scale, imageRect, mousePosition)) { + if (this.isMouseOverRectEdges(labelRects[i].rect, data)) { return labelRects[i]; } } return null; } - private isMouseOverRectEdges(rect: IRect, scale: number, imageRect: IRect, mousePosition: IPoint): boolean { + private isMouseOverRectEdges(rect: IRect, data: EditorData): boolean { const rectOnImage: IRect = RectUtil.translate( - this.calculateRectRelativeToActiveImage(rect, scale), imageRect); + this.calculateRectRelativeToActiveImage(rect, data), data.viewPortContentImageRect); const outerRectDelta: IPoint = { x: this.config.anchorHoverSize.width / 2, @@ -267,14 +266,14 @@ export class RectRenderEngine extends BaseRenderEngine { }; const innerRect: IRect = RectUtil.expand(rectOnImage, innerRectDelta); - return (RectUtil.isPointInside(outerRect, mousePosition) && - !RectUtil.isPointInside(innerRect, mousePosition)); + return (RectUtil.isPointInside(outerRect, data.mousePositionOnViewPortContent) && + !RectUtil.isPointInside(innerRect, data.mousePositionOnViewPortContent)); } private getAnchorUnderMouseByRect(rect: IRect, mousePosition: IPoint, imageRect: IRect): RectAnchor { const rectAnchors: RectAnchor[] = RectUtil.mapRectToAnchors(rect); for (let i = 0; i < rectAnchors.length; i++) { - const anchorRect: IRect = RectUtil.translate(RectUtil.getRectWithCenterAndSize(rectAnchors[i].position, this.config.anchorHoverSize), imageRect) + const anchorRect: IRect = RectUtil.translate(RectUtil.getRectWithCenterAndSize(rectAnchors[i].position, this.config.anchorHoverSize), imageRect); if (!!mousePosition && RectUtil.isPointInside(anchorRect, mousePosition)) { return rectAnchors[i]; } @@ -282,11 +281,11 @@ export class RectRenderEngine extends BaseRenderEngine { return null; } - private getAnchorUnderMouse(scale: number, mousePosition: IPoint, imageRect: IRect): RectAnchor { + private getAnchorUnderMouse(data: EditorData): RectAnchor { const labelRects: LabelRect[] = EditorSelector.getActiveImageData().labelRects; for (let i = 0; i < labelRects.length; i++) { - const rect: IRect = this.calculateRectRelativeToActiveImage(labelRects[i].rect, scale); - const rectAnchor = this.getAnchorUnderMouseByRect(rect, mousePosition, imageRect); + const rect: IRect = this.calculateRectRelativeToActiveImage(labelRects[i].rect, data); + const rectAnchor = this.getAnchorUnderMouseByRect(rect, data.mousePositionOnViewPortContent, data.viewPortContentImageRect); if (!!rectAnchor) return rectAnchor; } return null; @@ -295,14 +294,17 @@ export class RectRenderEngine extends BaseRenderEngine { private startRectCreation(mousePosition: IPoint) { this.startCreateRectPoint = mousePosition; store.dispatch(updateActiveLabelId(null)); + EditorActions.setViewPortActionsDisabledStatus(true); } private startRectResize(activatedAnchor: RectAnchor) { this.startResizeRectAnchor = activatedAnchor; + EditorActions.setViewPortActionsDisabledStatus(true); } private endRectTransformation() { this.startCreateRectPoint = null; this.startResizeRectAnchor = null; + EditorActions.setViewPortActionsDisabledStatus(false); } } \ No newline at end of file diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 46b45373..1e620984 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,3 +1,5 @@ +import {PopupWindowType} from "../data/enums/PopupWindowType"; + export class Settings { public static readonly GITHUB_URL: string = "https://github.com/SkalskiP"; public static readonly MEDIUM_URL: string = "https://medium.com/@piotr.skalski92"; @@ -5,9 +7,10 @@ export class Settings { public static readonly TOP_NAVIGATION_BAR_HEIGHT_PX: number = 35; public static readonly BOTTOM_NAVIGATION_BAR_HEIGHT_PX: number = 45 + 1; - public static readonly SIDE_NAVIGATION_BAR_WIDTH_CLOSED_PX: number = 20 + 1; + public static readonly SIDE_NAVIGATION_BAR_WIDTH_CLOSED_PX: number = 23 + 1; public static readonly SIDE_NAVIGATION_BAR_WIDTH_OPEN_PX: number = Settings.SIDE_NAVIGATION_BAR_WIDTH_CLOSED_PX + 300 + 1; public static readonly TOOLKIT_TAB_HEIGHT_PX: number = 40; + public static readonly TOOLBOX_PANEL_WIDTH_PX: number = 50 + 1; public static readonly EDITOR_MIN_WIDTH: number = 900; public static readonly EDITOR_MIN_HEIGHT: number = 500; @@ -18,12 +21,17 @@ export class Settings { public static readonly DARK_THEME_FIRST_COLOR: string = "#171717"; public static readonly DARK_THEME_SECOND_COLOR: string = "#282828"; public static readonly DARK_THEME_THIRD_COLOR: string = "#4c4c4c"; - public static readonly DARK_THEME_FORTH_COLOR: string = "#1f2c33"; + public static readonly DARK_THEME_FORTH_COLOR: string = "#262c2f"; - public static readonly CANVAS_PADDING_WIDTH_PX: number = 20; public static readonly CROSS_HAIR_THICKNESS_PX: number = 1; public static readonly CROSS_HAIR_COLOR: string = "#fff"; public static readonly RESIZE_HANDLE_DIMENSION_PX: number = 8; public static readonly RESIZE_HANDLE_HOVER_DIMENSION_PX = 16; + + public static readonly CLOSEABLE_POPUPS: PopupWindowType[] = [ + PopupWindowType.LOAD_IMAGES, + PopupWindowType.EXPORT_LABELS, + PopupWindowType.EXIT_PROJECT + ]; } \ No newline at end of file diff --git a/src/settings/ViewPointSettings.ts b/src/settings/ViewPointSettings.ts new file mode 100644 index 00000000..c264af9b --- /dev/null +++ b/src/settings/ViewPointSettings.ts @@ -0,0 +1,7 @@ +export class ViewPointSettings { + public static readonly CANVAS_MIN_MARGIN_PX: number = 20; + public static readonly MIN_ZOOM: number = 1; + public static readonly MAX_ZOOM: number = 4; + public static readonly ZOOM_STEP: number = 0.1; + public static readonly TRANSLATION_STEP_PX: number = 20; +} \ No newline at end of file diff --git a/src/settings/_Settings.scss b/src/settings/_Settings.scss index 4d8e6ecd..fe12fa47 100644 --- a/src/settings/_Settings.scss +++ b/src/settings/_Settings.scss @@ -1,7 +1,7 @@ $darkThemeFirstColor: #171717; $darkThemeSecondColor: #282828; $darkThemeThirdColor: #4c4c4c; -$darkThemeForthColor: #1f2c33; +$darkThemeForthColor: #262c2f; //$primaryColor: #33d1e5; //$secondaryColor: #39f5c2; @@ -15,6 +15,7 @@ $secondaryColor: #009efd; $topNavigationBarHeight: 35px; $stateBarHeight: 2px; -$sideNavigationBarCompanionWidth: 20px; +$sideNavigationBarCompanionWidth: 23px; $sideNavigationBarContentWidth: 300px; -$bottomNavigationBarHeight: 45px; \ No newline at end of file +$bottomNavigationBarHeight: 45px; +$toolboxWidth: 50px; \ No newline at end of file diff --git a/src/staticModels/EditorModel.ts b/src/staticModels/EditorModel.ts new file mode 100644 index 00000000..20933d7e --- /dev/null +++ b/src/staticModels/EditorModel.ts @@ -0,0 +1,32 @@ +import {PrimaryEditorRenderEngine} from "../logic/render/PrimaryEditorRenderEngine"; +import {BaseRenderEngine} from "../logic/render/BaseRenderEngine"; +import {IRect} from "../interfaces/IRect"; +import {IPoint} from "../interfaces/IPoint"; +import {ISize} from "../interfaces/ISize"; +import {ViewPointSettings} from "../settings/ViewPointSettings"; +import Scrollbars from "react-custom-scrollbars"; +import {ViewPortHelper} from "../logic/helpers/ViewPortHelper"; + +export class EditorModel { + public static editor: HTMLDivElement; + public static canvas: HTMLCanvasElement; + public static mousePositionIndicator: HTMLDivElement; + public static cursor: HTMLDivElement; + public static viewPortScrollbars: Scrollbars; + public static image: HTMLImageElement; + + public static primaryRenderingEngine: PrimaryEditorRenderEngine; + public static supportRenderingEngine: BaseRenderEngine; + + public static viewPortHelper: ViewPortHelper; + + public static isLoading: boolean = false; + public static viewPortActionsDisabled: boolean = false; + public static mousePositionOnViewPortContent: IPoint; + public static zoom: number = ViewPointSettings.MIN_ZOOM; + public static viewPortSize: ISize; + + // x and y describe the dimension of the margin that remains constant regardless of the scale of the image + // width and height describes the render image size for 100% scale + public static defaultRenderImageRect: IRect; +} \ No newline at end of file diff --git a/src/staticModels/PlatformModel.ts b/src/staticModels/PlatformModel.ts new file mode 100644 index 00000000..c7005845 --- /dev/null +++ b/src/staticModels/PlatformModel.ts @@ -0,0 +1,8 @@ +import {MobileDeviceData} from "../data/MobileDeviceData"; + +export class PlatformModel { + public static mobileDeviceData: MobileDeviceData; + public static isMac: boolean; + public static isSafari: boolean; + public static isFirefox: boolean; +} \ No newline at end of file diff --git a/src/store/Actions.ts b/src/store/Actions.ts index d4589723..85eb5d4a 100644 --- a/src/store/Actions.ts +++ b/src/store/Actions.ts @@ -12,8 +12,9 @@ export enum Action { UPDATE_ACTIVE_LABEL_ID = '@@UPDATE_ACTIVE_LABEL_ID', UPDATE_HIGHLIGHTED_LABEL_ID = '@@UPDATE_HIGHLIGHTED_LABEL_ID', UPDATE_LABEL_NAMES_LIST = '@@UPDATE_LABEL_NAMES_LIST', - UPDATE_MOBILE_DEVICE_DATA = '@@UPDATE_MOBILE_DEVICE_DATA', UPDATE_FIRST_LABEL_CREATED_FLAG = '@@UPDATE_FIRST_LABEL_CREATED_FLAG', UPDATE_CUSTOM_CURSOR_STYLE = '@@UPDATE_CUSTOM_CURSOR_STYLE', + UPDATE_PREVENT_CUSTOM_CURSOR_STATUS = '@@UPDATE_PREVENT_CUSTOM_CURSOR_STATUS', + UPDATE_IMAGE_DRAG_MODE_STATUS = '@@UPDATE_IMAGE_DRAG_MODE_STATUS', UPDATE_CONTEXT = '@@UPDATE_CONTEXT' } \ No newline at end of file diff --git a/src/store/general/actionCreators.ts b/src/store/general/actionCreators.ts index 876961d8..f54909cd 100644 --- a/src/store/general/actionCreators.ts +++ b/src/store/general/actionCreators.ts @@ -2,7 +2,6 @@ import {ISize} from "../../interfaces/ISize"; import {GeneralActionTypes} from "./types"; import {Action} from "../Actions"; import {PopupWindowType} from "../../data/enums/PopupWindowType"; -import {MobileDeviceData} from "../../data/MobileDeviceData"; import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; import {ContextType} from "../../data/enums/ContextType"; @@ -24,15 +23,6 @@ export function updateActivePopupType(activePopupType: PopupWindowType): General } } -export function updateMobileDeviceData(mobileDeviceData: MobileDeviceData): GeneralActionTypes { - return { - type: Action.UPDATE_MOBILE_DEVICE_DATA, - payload: { - mobileDeviceData, - } - } -} - export function updateCustomCursorStyle(customCursorStyle: CustomCursorStyle): GeneralActionTypes { return { type: Action.UPDATE_CUSTOM_CURSOR_STYLE, @@ -49,4 +39,22 @@ export function updateActiveContext(activeContext: ContextType): GeneralActionTy activeContext, }, }; +} + +export function updatePreventCustomCursorStatus(preventCustomCursor: boolean): GeneralActionTypes { + return { + type: Action.UPDATE_PREVENT_CUSTOM_CURSOR_STATUS, + payload: { + preventCustomCursor, + }, + }; +} + +export function updateImageDragModeStatus(imageDragMode: boolean): GeneralActionTypes { + return { + type: Action.UPDATE_IMAGE_DRAG_MODE_STATUS, + payload: { + imageDragMode, + }, + }; } \ No newline at end of file diff --git a/src/store/general/reducer.ts b/src/store/general/reducer.ts index 3d8e1705..033076bd 100644 --- a/src/store/general/reducer.ts +++ b/src/store/general/reducer.ts @@ -5,9 +5,10 @@ import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; const initialState: GeneralState = { windowSize: null, activePopupType: null, - mobileDeviceData: null, customCursorStyle: CustomCursorStyle.DEFAULT, - activeContext: null + activeContext: null, + preventCustomCursor: false, + imageDragMode: false }; export function generalReducer( @@ -27,12 +28,6 @@ export function generalReducer( activePopupType: action.payload.activePopupType } } - case Action.UPDATE_MOBILE_DEVICE_DATA: { - return { - ...state, - mobileDeviceData: action.payload.mobileDeviceData - } - } case Action.UPDATE_CUSTOM_CURSOR_STYLE: { return { ...state, @@ -45,6 +40,18 @@ export function generalReducer( activeContext: action.payload.activeContext } } + case Action.UPDATE_PREVENT_CUSTOM_CURSOR_STATUS: { + return { + ...state, + preventCustomCursor: action.payload.preventCustomCursor + } + } + case Action.UPDATE_IMAGE_DRAG_MODE_STATUS: { + return { + ...state, + imageDragMode: action.payload.imageDragMode + } + } default: return state; } diff --git a/src/store/general/types.ts b/src/store/general/types.ts index 59c6b29b..a69f9ba6 100644 --- a/src/store/general/types.ts +++ b/src/store/general/types.ts @@ -1,15 +1,15 @@ import {ISize} from "../../interfaces/ISize"; import {Action} from "../Actions"; import {PopupWindowType} from "../../data/enums/PopupWindowType"; -import {MobileDeviceData} from "../../data/MobileDeviceData"; import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; import {ContextType} from "../../data/enums/ContextType"; export type GeneralState = { windowSize: ISize; - mobileDeviceData: MobileDeviceData; activePopupType: PopupWindowType; customCursorStyle: CustomCursorStyle; + preventCustomCursor: boolean; + imageDragMode: boolean; activeContext: ContextType; } @@ -27,13 +27,6 @@ interface UpdateActivePopupType { } } -interface UpdateMobileDeviceData { - type: typeof Action.UPDATE_MOBILE_DEVICE_DATA; - payload: { - mobileDeviceData: MobileDeviceData; - } -} - interface UpdateCustomCursorStyle { type: typeof Action.UPDATE_CUSTOM_CURSOR_STYLE; payload: { @@ -48,8 +41,23 @@ interface UpdateActiveContext { } } +interface UpdatePreventCustomCursorStatus { + type: typeof Action.UPDATE_PREVENT_CUSTOM_CURSOR_STATUS; + payload: { + preventCustomCursor: boolean; + } +} + +interface UpdateImageDragModeStatus { + type: typeof Action.UPDATE_IMAGE_DRAG_MODE_STATUS; + payload: { + imageDragMode: boolean; + } +} + export type GeneralActionTypes = UpdateWindowSize | UpdateActivePopupType - | UpdateMobileDeviceData | UpdateCustomCursorStyle - | UpdateActiveContext \ No newline at end of file + | UpdateActiveContext + | UpdatePreventCustomCursorStatus + | UpdateImageDragModeStatus \ No newline at end of file diff --git a/src/store/selectors/EditorSelector.ts b/src/store/selectors/EditorSelector.ts index d7ea527d..4828cfab 100644 --- a/src/store/selectors/EditorSelector.ts +++ b/src/store/selectors/EditorSelector.ts @@ -1,6 +1,7 @@ import {store} from "../.."; import {ImageData, LabelPoint, LabelPolygon, LabelRect} from "../editor/types"; import _ from "lodash"; +import {LabelType} from "../../data/enums/LabelType"; export class EditorSelector { public static getProjectName(): string { @@ -15,6 +16,10 @@ export class EditorSelector { return store.getState().editor.activeLabelNameIndex; } + public static getActiveLabelType(): LabelType { + return store.getState().editor.activeLabelType; + } + public static getImagesData(): ImageData[] { return store.getState().editor.imagesData; } @@ -29,8 +34,17 @@ export class EditorSelector { if (activeImageIndex === null) return null; + return EditorSelector.getImageDataByIndex(activeImageIndex); + } + + public static getImageDataByIndex(index: number): ImageData { + const imagesData: ImageData[] = EditorSelector.getImagesData(); + return imagesData[index]; + } + + public static getImageDataById(id: string): ImageData { const imagesData: ImageData[] = EditorSelector.getImagesData(); - return imagesData[activeImageIndex]; + return _.find(imagesData, {id: id}); } public static getActiveLabelId(): string | null { diff --git a/src/store/selectors/GeneralSelector.ts b/src/store/selectors/GeneralSelector.ts index d8ce27bd..850e9f39 100644 --- a/src/store/selectors/GeneralSelector.ts +++ b/src/store/selectors/GeneralSelector.ts @@ -1,8 +1,26 @@ import {store} from "../.."; import {PopupWindowType} from "../../data/enums/PopupWindowType"; +import {ContextType} from "../../data/enums/ContextType"; +import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; export class GeneralSelector { public static getActivePopupType(): PopupWindowType { return store.getState().general.activePopupType; } + + public static getActiveContext(): ContextType { + return store.getState().general.activeContext; + } + + public static getPreventCustomCursorStatus(): boolean { + return store.getState().general.preventCustomCursor; + } + + public static getImageDragModeStatus(): boolean { + return store.getState().general.imageDragMode; + } + + public static getCustomCursorStyle(): CustomCursorStyle { + return store.getState().general.customCursorStyle; + } } \ No newline at end of file diff --git a/src/utils/DirectionUtil.ts b/src/utils/DirectionUtil.ts new file mode 100644 index 00000000..c6f6872d --- /dev/null +++ b/src/utils/DirectionUtil.ts @@ -0,0 +1,29 @@ +import {Direction} from "../data/enums/Direction"; +import {IPoint} from "../interfaces/IPoint"; + +export class DirectionUtil { + public static convertDirectionToVector(direction: Direction): IPoint { + switch (direction) { + case Direction.RIGHT: + return {x: 1, y: 0}; + case Direction.LEFT: + return {x: -1, y: 0}; + case Direction.TOP: + return {x: 0, y: 1}; + case Direction.BOTTOM: + return {x: 0, y: -1}; + case Direction.TOP_RIGHT: + return {x: 1, y: 1}; + case Direction.TOP_LEFT: + return {x: -1, y: 1}; + case Direction.BOTTOM_RIGHT: + return {x: 1, y: -1}; + case Direction.BOTTOM_LEFT: + return {x: -1, y: -1}; + case Direction.CENTER: + return {x: 0, y: 0}; + default: + return null; + } + } +} \ No newline at end of file diff --git a/src/utils/EditorUtil.ts b/src/utils/EditorUtil.ts index 5746d4e7..d03dc41d 100644 --- a/src/utils/EditorUtil.ts +++ b/src/utils/EditorUtil.ts @@ -14,6 +14,10 @@ export class EditorUtil { return "ico/move.png"; case CustomCursorStyle.CANCEL: return "ico/cancel.png"; + case CustomCursorStyle.GRAB: + return "ico/hand-fill.png"; + case CustomCursorStyle.GRABBING: + return "ico/hand-fill-grab.png"; default: return null; } @@ -27,6 +31,8 @@ export class EditorUtil { "resize": cursorStyle === CustomCursorStyle.RESIZE, "close": cursorStyle === CustomCursorStyle.CLOSE, "cancel": cursorStyle === CustomCursorStyle.CANCEL, + "grab": cursorStyle === CustomCursorStyle.GRAB, + "grabbing": cursorStyle === CustomCursorStyle.GRABBING } ); }; diff --git a/src/utils/ImageUtil.ts b/src/utils/ImageUtil.ts new file mode 100644 index 00000000..15eafd69 --- /dev/null +++ b/src/utils/ImageUtil.ts @@ -0,0 +1,11 @@ +import {ISize} from "../interfaces/ISize"; + +export class ImageUtil { + public static getSize(image: HTMLImageElement): ISize { + if (!image) return null; + return { + width: image.width, + height: image.height + } + } +} \ No newline at end of file diff --git a/src/utils/NumberUtil.ts b/src/utils/NumberUtil.ts index e5ab948d..3f6f6476 100644 --- a/src/utils/NumberUtil.ts +++ b/src/utils/NumberUtil.ts @@ -7,4 +7,8 @@ export class NumberUtil { return value; } + + public static isValueInRange(value: number, min: number, max: number): boolean { + return value >= min && value <= max; + } } \ No newline at end of file diff --git a/src/utils/PlatformUtil.ts b/src/utils/PlatformUtil.ts new file mode 100644 index 00000000..e5b703ad --- /dev/null +++ b/src/utils/PlatformUtil.ts @@ -0,0 +1,25 @@ +import {MobileDeviceData} from "../data/MobileDeviceData"; +import MobileDetect from 'mobile-detect' + +export class PlatformUtil { + public static getMobileDeviceData(userAgent: string): MobileDeviceData { + const mobileDetect = new MobileDetect(userAgent); + return { + manufacturer: mobileDetect.mobile(), + browser: mobileDetect.userAgent(), + os: mobileDetect.os() + } + } + + public static isMac(userAgent: string): boolean { + return !!userAgent.toLowerCase().match("mac"); + } + + public static isSafari(userAgent: string): boolean { + return !!userAgent.toLowerCase().match("safari"); + } + + public static isFirefox(userAgent: string): boolean { + return !!userAgent.toLowerCase().match("firefox"); + } +} \ No newline at end of file diff --git a/src/utils/RenderEngineUtil.ts b/src/utils/RenderEngineUtil.ts index a24cd4e1..b1cd73f4 100644 --- a/src/utils/RenderEngineUtil.ts +++ b/src/utils/RenderEngineUtil.ts @@ -8,37 +8,53 @@ import {PointUtil} from "./PointUtil"; import {IRect} from "../interfaces/IRect"; export class RenderEngineUtil { + public static calculateImageScale(data: EditorData): number { + return data.realImageSize.width / data.viewPortContentImageRect.width; + } public static isMouseOverImage(data: EditorData): boolean { - return RectUtil.isPointInside(data.activeImageRectOnCanvas, data.mousePositionOnCanvas); + return RectUtil.isPointInside(data.viewPortContentImageRect, data.mousePositionOnViewPortContent); } public static isMouseOverCanvas(data: EditorData): boolean { - return RectUtil.isPointInside({x: 0, y: 0, ...data.canvasSize}, data.mousePositionOnCanvas); + return RectUtil.isPointInside({x: 0, y: 0, ...data.viewPortContentSize}, data.mousePositionOnViewPortContent); + } + + public static transferPolygonFromImageToViewPortContent(polygon: IPoint[], data: EditorData): IPoint[] { + return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromImageToViewPortContent(point, data)); } - public static transferPolygonFromImageToCanvas(polygon: IPoint[], data: EditorData): IPoint[] { - return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromImageToCanvas(point, data)); + public static transferPointFromImageToViewPortContent(point: IPoint, data: EditorData): IPoint { + const scale = RenderEngineUtil.calculateImageScale(data); + return PointUtil.add(PointUtil.multiply(point, 1/scale), data.viewPortContentImageRect); } - public static transferPointFromImageToCanvas(point: IPoint, data: EditorData): IPoint { - return PointUtil.add(PointUtil.multiply(point, 1/data.activeImageScale), data.activeImageRectOnCanvas); + public static transferPolygonFromViewPortContentToImage(polygon: IPoint[], data: EditorData): IPoint[] { + return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromViewPortContentToImage(point, data)); } - public static transferPolygonFromCanvasToImage(polygon: IPoint[], data: EditorData): IPoint[] { - return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromCanvasToImage(point, data)); + public static transferPointFromViewPortContentToImage(point: IPoint, data: EditorData): IPoint { + const scale = RenderEngineUtil.calculateImageScale(data); + return PointUtil.multiply(PointUtil.subtract(point, data.viewPortContentImageRect), scale); } - public static transferPointFromCanvasToImage(point: IPoint, data: EditorData): IPoint { - return PointUtil.multiply(PointUtil.subtract(point, data.activeImageRectOnCanvas), data.activeImageScale); + public static transferRectFromViewPortContentToImage(rect: IRect, data: EditorData): IRect { + const scale = RenderEngineUtil.calculateImageScale(data); + return RectUtil.translate(RectUtil.scaleRect(rect, 1/scale), data.viewPortContentImageRect); } - public static transferRectFromCanvasToImage(rect: IRect, data: EditorData): IRect { - return RectUtil.translate(RectUtil.scaleRect(rect, 1/data.activeImageScale), data.activeImageRectOnCanvas); + public static transferRectFromImageToViewPortContent(rect: IRect, data: EditorData): IRect { + const scale = RenderEngineUtil.calculateImageScale(data); + const translation: IPoint = { + x: - data.viewPortContentImageRect.x, + y: - data.viewPortContentImageRect.y + }; + + return RectUtil.scaleRect(RectUtil.translate(rect, translation), scale); } public static wrapDefaultCursorStyleInCancel(data: EditorData) { - if (RectUtil.isPointInside(data.activeImageRectOnCanvas, data.mousePositionOnCanvas)) { + if (RectUtil.isPointInside(data.viewPortContentImageRect, data.mousePositionOnViewPortContent)) { store.dispatch(updateCustomCursorStyle(CustomCursorStyle.DEFAULT)); } else { store.dispatch(updateCustomCursorStyle(CustomCursorStyle.CANCEL)); diff --git a/src/utils/SizeUtil.ts b/src/utils/SizeUtil.ts new file mode 100644 index 00000000..cbcdc4cf --- /dev/null +++ b/src/utils/SizeUtil.ts @@ -0,0 +1,10 @@ +import {ISize} from "../interfaces/ISize"; + +export class SizeUtil { + public static scale(size: ISize, scale: number): ISize { + return { + width: size.width * scale, + height: size.height * scale + } + } +} \ No newline at end of file diff --git a/src/views/Common/ImageButton/ImageButton.tsx b/src/views/Common/ImageButton/ImageButton.tsx index e9ebc599..e18128c8 100644 --- a/src/views/Common/ImageButton/ImageButton.tsx +++ b/src/views/Common/ImageButton/ImageButton.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import {ISize} from "../../../interfaces/ISize"; import './ImageButton.scss'; import classNames from "classnames"; +import {LegacyRef} from "react"; -interface Props { - size:ISize, +export interface ImageButtonProps extends React.HTMLProps { + buttonSize:ISize, padding?:number; image:string, imageAlt:string, @@ -16,8 +17,8 @@ interface Props { externalClassName?:string; } -export const ImageButton = (props:Props) => { - const {size, padding, image, imageAlt, href, onClick, style, isActive, isDisabled, externalClassName} = props; +export const ImageButton = React.forwardRef((props: ImageButtonProps, ref: LegacyRef) => { + const {buttonSize, padding, image, imageAlt, href, onClick, style, isActive, isDisabled, externalClassName} = props; const imagePadding:number = !!padding ? padding : 10; const onClickHandler = (event: React.MouseEvent) => { @@ -27,13 +28,13 @@ export const ImageButton = (props:Props) => { const buttonStyle:React.CSSProperties = { ...style, - width: size.width, - height: size.height + width: buttonSize.width, + height: buttonSize.height }; const imageStyle:React.CSSProperties = { - maxWidth: size.width - imagePadding, - maxHeight: size.height - imagePadding + maxWidth: buttonSize.width - imagePadding, + maxHeight: buttonSize.height - imagePadding }; const getClassName = () => { @@ -48,7 +49,12 @@ export const ImageButton = (props:Props) => { }; return( -
+
{!!href && { />}
); -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/src/views/EditorView/BottomNavigationBar/BottomNavigationBar.tsx b/src/views/EditorView/BottomNavigationBar/BottomNavigationBar.tsx index 8b3aea34..23ce4794 100644 --- a/src/views/EditorView/BottomNavigationBar/BottomNavigationBar.tsx +++ b/src/views/EditorView/BottomNavigationBar/BottomNavigationBar.tsx @@ -1,13 +1,13 @@ import React from 'react'; import './BottomNavigationBar.scss'; import {ImageData} from "../../../store/editor/types"; -import {updateActiveImageIndex} from "../../../store/editor/actionCreators"; import {AppState} from "../../../store"; import {connect} from "react-redux"; import {ImageButton} from "../../Common/ImageButton/ImageButton"; import {ISize} from "../../../interfaces/ISize"; import {ContextType} from "../../../data/enums/ContextType"; import classNames from "classnames"; +import {ImageActions} from "../../../logic/actions/ImageActions"; interface IProps { size: ISize; @@ -15,22 +15,10 @@ interface IProps { totalImageCount: number; activeImageIndex: number; activeContext: ContextType; - updateActiveImageIndex: (activeImageIndex: number) => any; } -const BottomNavigationBar: React.FC = ({size, imageData, totalImageCount, activeImageIndex, activeContext, updateActiveImageIndex}) => { +const BottomNavigationBar: React.FC = ({size, imageData, totalImageCount, activeImageIndex, activeContext}) => { const minWidth:number = 400; - const viewPreviousImage = () => { - if (activeImageIndex > 0) { - updateActiveImageIndex(activeImageIndex - 1) - } - }; - - const viewNextImage = () => { - if (activeImageIndex < totalImageCount - 1) { - updateActiveImageIndex(activeImageIndex + 1) - } - }; const getImageCounter = () => { return (activeImageIndex + 1) + " / " + totalImageCount; @@ -50,8 +38,8 @@ const BottomNavigationBar: React.FC = ({size, imageData, totalImageCount ImageActions.getPreviousImage()} isDisabled={activeImageIndex === 0} externalClassName={"left"} /> @@ -62,8 +50,8 @@ const BottomNavigationBar: React.FC = ({size, imageData, totalImageCount ImageActions.getNextImage()} isDisabled={activeImageIndex === totalImageCount - 1} externalClassName={"right"} /> @@ -71,9 +59,7 @@ const BottomNavigationBar: React.FC = ({size, imageData, totalImageCount ); }; -const mapDispatchToProps = { - updateActiveImageIndex -}; +const mapDispatchToProps = {}; const mapStateToProps = (state: AppState) => ({ activeImageIndex: state.editor.activeImageIndex, diff --git a/src/views/EditorView/Editor/Editor.scss b/src/views/EditorView/Editor/Editor.scss index 584ba217..a0c97f76 100644 --- a/src/views/EditorView/Editor/Editor.scss +++ b/src/views/EditorView/Editor/Editor.scss @@ -5,15 +5,27 @@ flex: 1; position: relative; - .ImageCanvas { - position: absolute; - top: 0; - left: 0; - cursor: none; + .ViewPortContent { + position: relative; + + .track-horizontal { + cursor: none; + } - &:hover { + .track-vertical { cursor: none; } + + .ImageCanvas { + position: absolute; + top: 0; + left: 0; + cursor: none; + + &:hover { + cursor: none; + } + } } .MousePositionIndicator { @@ -30,7 +42,7 @@ } .Cursor { - position: relative; + position: absolute; width: 6px; height: 6px; transition: width 0.05s ease-out, height 0.05s ease-out, background-color 0.05s ease-in; @@ -58,7 +70,14 @@ background-color: transparent; } - &.move, &.add, &.resize, &.close, &.cancel { + &.grabbing { + width: 18px; + height: 18px; + background-color: rgba(255, 255, 255, 0.5); + border: 2px solid transparent; + } + + &.move, &.add, &.resize, &.close, &.cancel, &.grab, &.grabbing { > img { display: block; } diff --git a/src/views/EditorView/Editor/Editor.tsx b/src/views/EditorView/Editor/Editor.tsx index d25086e0..2461f033 100644 --- a/src/views/EditorView/Editor/Editor.tsx +++ b/src/views/EditorView/Editor/Editor.tsx @@ -14,11 +14,14 @@ import {CustomCursorStyle} from "../../../data/enums/CustomCursorStyle"; import {ImageLoadManager} from "../../../logic/imageRepository/ImageLoadManager"; import {EventType} from "../../../data/enums/EventType"; import {EditorData} from "../../../data/EditorData"; -import {EditorModel} from "../../../model/EditorModel"; +import {EditorModel} from "../../../staticModels/EditorModel"; import {EditorActions} from "../../../logic/actions/EditorActions"; import {EditorUtil} from "../../../utils/EditorUtil"; import {ContextManager} from "../../../logic/context/ContextManager"; import {ContextType} from "../../../data/enums/ContextType"; +import Scrollbars from 'react-custom-scrollbars'; +import {ViewPortActions} from "../../../logic/actions/ViewPortActions"; +import {PlatformModel} from "../../../staticModels/PlatformModel"; interface IProps { size: ISize; @@ -28,6 +31,7 @@ interface IProps { activePopupType: PopupWindowType; activeLabelId: string; customCursorStyle: CustomCursorStyle; + imageDragMode: boolean; } class Editor extends React.Component { @@ -42,8 +46,9 @@ class Editor extends React.Component { const {imageData, activeLabelType} = this.props; ContextManager.switchCtx(ContextType.EDITOR); - EditorActions.mountRenderEngines(activeLabelType); + EditorActions.mountRenderEnginesAndHelpers(activeLabelType); ImageLoadManager.addAndRun(this.loadImage(imageData)); + ViewPortActions.resizeCanvas(this.props.size); } public componentWillUnmount(): void { @@ -56,7 +61,7 @@ class Editor extends React.Component { prevProps.imageData.id !== imageData.id && ImageLoadManager.addAndRun(this.loadImage(imageData)); prevProps.activeLabelType !== activeLabelType && EditorActions.swapSupportRenderingEngine(activeLabelType); - this.updateModelAndRender() + this.updateModelAndRender(); } // ================================================================================================================= @@ -67,12 +72,14 @@ class Editor extends React.Component { window.addEventListener(EventType.MOUSE_MOVE, this.update); window.addEventListener(EventType.MOUSE_UP, this.update); EditorModel.canvas.addEventListener(EventType.MOUSE_DOWN, this.update); + EditorModel.canvas.addEventListener(EventType.MOUSE_WHEEL, this.handleZoom); } private unmountEventListeners() { window.removeEventListener(EventType.MOUSE_MOVE, this.update); window.removeEventListener(EventType.MOUSE_UP, this.update); EditorModel.canvas.removeEventListener(EventType.MOUSE_DOWN, this.update); + EditorModel.canvas.removeEventListener(EventType.MOUSE_WHEEL, this.handleZoom); } // ================================================================================================================= @@ -109,29 +116,64 @@ class Editor extends React.Component { // ================================================================================================================= private updateModelAndRender = () => { - EditorActions.resizeCanvas(this.props.size); - EditorActions.calculateActiveImageCharacteristics(); + ViewPortActions.updateViewPortSize(); + ViewPortActions.updateDefaultViewPortImageRect(); + ViewPortActions.resizeViewPortContent(); EditorActions.fullRender(); }; private update = (event: MouseEvent) => { const editorData: EditorData = EditorActions.getEditorData(event); - EditorModel.mousePositionOnCanvas = CanvasUtil.getMousePositionOnCanvasFromEvent(event, EditorModel.canvas); + EditorModel.mousePositionOnViewPortContent = CanvasUtil.getMousePositionOnCanvasFromEvent(event, EditorModel.canvas); EditorModel.primaryRenderingEngine.update(editorData); - EditorModel.supportRenderingEngine && EditorModel.supportRenderingEngine.update(editorData); + + if (this.props.imageDragMode) { + EditorModel.viewPortHelper.update(editorData); + } else { + EditorModel.supportRenderingEngine && EditorModel.supportRenderingEngine.update(editorData); + } + !this.props.activePopupType && EditorActions.updateMousePositionIndicator(event); EditorActions.fullRender(); }; + private handleZoom = (event: MouseWheelEvent) => { + if (event.ctrlKey || (PlatformModel.isMac && event.metaKey)) { + const scrollSign: number = Math.sign(event.deltaY); + if ((PlatformModel.isMac && scrollSign === -1) || (!PlatformModel.isMac && scrollSign === 1)) { + ViewPortActions.zoomOut(); + } + else if ((PlatformModel.isMac && scrollSign === 1) || (!PlatformModel.isMac && scrollSign === -1)) { + ViewPortActions.zoomIn(); + } + } + }; + public render() { return ( -
- EditorModel.canvas = ref} - draggable={false} - onContextMenu={(event: React.MouseEvent) => event.preventDefault()} - /> +
EditorModel.editor = ref} + draggable={false} + > + EditorModel.viewPortScrollbars = ref} + renderTrackHorizontal={props =>
} + renderTrackVertical={props =>
} + // renderThumbHorizontal={props =>
} + // renderThumbVertical={props =>
} + > +
+ EditorModel.canvas = ref} + draggable={false} + onContextMenu={(event: React.MouseEvent) => event.preventDefault()} + /> +
+
EditorModel.mousePositionIndicator = ref} @@ -161,7 +203,8 @@ const mapStateToProps = (state: AppState) => ({ activeLabelType: state.editor.activeLabelType, activePopupType: state.general.activePopupType, activeLabelId: state.editor.activeLabelId, - customCursorStyle: state.general.customCursorStyle + customCursorStyle: state.general.customCursorStyle, + imageDragMode: state.general.imageDragMode }); export default connect( diff --git a/src/views/EditorView/EditorContainer/EditorContainer.scss b/src/views/EditorView/EditorContainer/EditorContainer.scss index 8ffbe61d..3d92eeb3 100644 --- a/src/views/EditorView/EditorContainer/EditorContainer.scss +++ b/src/views/EditorView/EditorContainer/EditorContainer.scss @@ -22,5 +22,17 @@ justify-content: center; align-items: center; align-content: center; + + .EditorNToolBox { + align-self: stretch; + flex: 1; + min-height: 0; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; + } } } \ No newline at end of file diff --git a/src/views/EditorView/EditorContainer/EditorContainer.tsx b/src/views/EditorView/EditorContainer/EditorContainer.tsx index 2d04b36e..61fb6ffd 100644 --- a/src/views/EditorView/EditorContainer/EditorContainer.tsx +++ b/src/views/EditorView/EditorContainer/EditorContainer.tsx @@ -14,14 +14,16 @@ import Editor from "../Editor/Editor"; import BottomNavigationBar from "../BottomNavigationBar/BottomNavigationBar"; import {ContextManager} from "../../../logic/context/ContextManager"; import {ContextType} from "../../../data/enums/ContextType"; +import ToolBox from "../ToolBox/ToolBox"; interface IProps { windowSize: ISize; activeImageIndex: number; imagesData: ImageData[]; + activeContext: ContextType; } -const EditorContainer: React.FC = ({windowSize, activeImageIndex, imagesData}) => { +const EditorContainer: React.FC = ({windowSize, activeImageIndex, imagesData, activeContext}) => { const [leftTabStatus, setLeftTabStatus] = useState(true); const [rightTabStatus, setRightTabStatus] = useState(true); @@ -38,13 +40,22 @@ const EditorContainer: React.FC = ({windowSize, activeImageIndex, images return null; }; + const leftSideBarButtonOnClick = () => { + if (!leftTabStatus) + ContextManager.switchCtx(ContextType.LEFT_NAVBAR); + else if (leftTabStatus && activeContext === ContextType.LEFT_NAVBAR) + ContextManager.restoreCtx(); + + setLeftTabStatus(!leftTabStatus); + }; + const leftSideBarCompanionRender = () => { return <> setLeftTabStatus(!leftTabStatus)} + onClick={leftSideBarButtonOnClick} isActive={leftTabStatus} /> @@ -54,13 +65,22 @@ const EditorContainer: React.FC = ({windowSize, activeImageIndex, images return }; + const rightSideBarButtonOnClick = () => { + if (!rightTabStatus) + ContextManager.switchCtx(ContextType.RIGHT_NAVBAR); + else if (rightTabStatus && activeContext === ContextType.RIGHT_NAVBAR) + ContextManager.restoreCtx(); + + setRightTabStatus(!rightTabStatus); + }; + const rightSideBarCompanionRender = () => { return <> setRightTabStatus(!rightTabStatus)} + onClick={rightSideBarButtonOnClick} isActive={rightTabStatus} /> @@ -75,16 +95,24 @@ const EditorContainer: React.FC = ({windowSize, activeImageIndex, images
ContextManager.switchCtx(ContextType.EDITOR)} > - +
+ + +
= ({windowSize, activeImageIndex, images @@ -104,7 +133,8 @@ const EditorContainer: React.FC = ({windowSize, activeImageIndex, images const mapStateToProps = (state: AppState) => ({ windowSize: state.general.windowSize, activeImageIndex: state.editor.activeImageIndex, - imagesData: state.editor.imagesData + imagesData: state.editor.imagesData, + activeContext: state.general.activeContext }); export default connect( diff --git a/src/views/EditorView/EditorView.tsx b/src/views/EditorView/EditorView.tsx index 1f13cc3a..370da906 100644 --- a/src/views/EditorView/EditorView.tsx +++ b/src/views/EditorView/EditorView.tsx @@ -23,7 +23,10 @@ const EditorView: React.FC = ({activePopupType}) => { }; return ( -
+
diff --git a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx index 6227c695..bf421de1 100644 --- a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx +++ b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx @@ -3,17 +3,18 @@ import {connect} from "react-redux"; import {LabelType} from "../../../../data/enums/LabelType"; import {ISize} from "../../../../interfaces/ISize"; import {AppState} from "../../../../store"; -import {updateActiveImageIndex, updateActiveLabelId} from "../../../../store/editor/actionCreators"; import {ImageData} from "../../../../store/editor/types"; import {VirtualList} from "../../../Common/VirtualList/VirtualList"; import ImagePreview from "../ImagePreview/ImagePreview"; import './ImagesList.scss'; +import {ContextManager} from "../../../../logic/context/ContextManager"; +import {ContextType} from "../../../../data/enums/ContextType"; +import {ImageActions} from "../../../../logic/actions/ImageActions"; +import {EventType} from "../../../../data/enums/EventType"; interface IProps { activeImageIndex: number; imagesData: ImageData[]; - updateActiveImageIndex: (activeImageIndex: number) => any; - updateActiveLabelId: (activeLabelId: string) => any; activeLabelType: LabelType; } @@ -34,11 +35,11 @@ class ImagesList extends React.Component { public componentDidMount(): void { this.updateListSize(); - window.addEventListener("resize", this.updateListSize); + window.addEventListener(EventType.RESIZE, this.updateListSize); } public componentWillUnmount(): void { - window.removeEventListener("resize", this.updateListSize); + window.removeEventListener(EventType.RESIZE, this.updateListSize); } private updateListSize = () => { @@ -55,8 +56,7 @@ class ImagesList extends React.Component { }; private onClickHandler = (index: number) => { - this.props.updateActiveImageIndex(index); - this.props.updateActiveLabelId(null); + ImageActions.getImageByIndex(index) }; private renderImagePreview = (index: number, isScrolling: boolean, isVisible: boolean, style: React.CSSProperties) => { @@ -79,7 +79,11 @@ class ImagesList extends React.Component { public render() { const { size } = this.state; return( -
this.imagesListRef = ref}> +
this.imagesListRef = ref} + onClick={() => ContextManager.switchCtx(ContextType.LEFT_NAVBAR)} + > {!!size && { } } -const mapDispatchToProps = { - updateActiveImageIndex, - updateActiveLabelId -}; +const mapDispatchToProps = {}; const mapStateToProps = (state: AppState) => ({ activeImageIndex: state.editor.activeImageIndex, diff --git a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx index 6d6791df..6fc12138 100644 --- a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx +++ b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx @@ -10,6 +10,7 @@ import {AppState} from "../../../../store"; import {connect} from "react-redux"; import {updateActiveLabelId, updateHighlightedLabelId} from "../../../../store/editor/actionCreators"; import Scrollbars from 'react-custom-scrollbars'; +import {EventType} from "../../../../data/enums/EventType"; interface IProps { size: ISize; @@ -63,7 +64,7 @@ class LabelInputField extends React.Component { private openDropdown = () => { this.setState({isOpen: true}); - window.addEventListener("mousedown", this.closeDropdown); + window.addEventListener(EventType.MOUSE_DOWN, this.closeDropdown); }; private closeDropdown = (event: MouseEvent) => { @@ -78,7 +79,7 @@ class LabelInputField extends React.Component { if (!RectUtil.isPointInside(dropDownRect, mousePosition)) { this.setState({isOpen: false}); - window.removeEventListener("mousedown", this.closeDropdown) + window.removeEventListener(EventType.MOUSE_DOWN, this.closeDropdown) } }; @@ -100,7 +101,7 @@ class LabelInputField extends React.Component { private getDropdownOptions = () => { const onClick = (index: number, event: React.MouseEvent) => { this.setState({isOpen: false}); - window.removeEventListener("mousedown", this.closeDropdown); + window.removeEventListener(EventType.MOUSE_DOWN, this.closeDropdown); this.props.onSelectLabel(this.props.id, index); this.props.updateHighlightedLabelId(null); this.props.updateActiveLabelId(this.props.id); @@ -180,7 +181,7 @@ class LabelInputField extends React.Component { externalClassName={"trash"} image={"ico/trash.png"} imageAlt={"remove_rect"} - size={{width: 30, height: 30}} + buttonSize={{width: 30, height: 30}} onClick={() => onDelete(id)} />
diff --git a/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx b/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx index 922875ed..6b0a5f27 100644 --- a/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx +++ b/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx @@ -14,6 +14,9 @@ import {Settings} from "../../../../settings/Settings"; import RectLabelsList from "../RectLabelsList/RectLabelsList"; import PointLabelsList from "../PointLabelsList/PointLabelsList"; import PolygonLabelsList from "../PolygonLabelsList/PolygonLabelsList"; +import {ContextManager} from "../../../../logic/context/ContextManager"; +import {ContextType} from "../../../../data/enums/ContextType"; +import {EventType} from "../../../../data/enums/EventType"; interface IProps { activeImageIndex:number, @@ -58,11 +61,11 @@ class LabelsToolkit extends React.Component { public componentDidMount(): void { this.updateToolkitSize(); - window.addEventListener("resize", this.updateToolkitSize); + window.addEventListener(EventType.RESIZE, this.updateToolkitSize); } public componentWillUnmount(): void { - window.removeEventListener("resize", this.updateToolkitSize); + window.removeEventListener(EventType.RESIZE, this.updateToolkitSize); } private updateToolkitSize = () => { @@ -164,6 +167,7 @@ class LabelsToolkit extends React.Component {
this.labelsToolkitRef = ref} + onClick={() => ContextManager.switchCtx(ContextType.RIGHT_NAVBAR)} > {this.state.size && this.renderChildren()}
diff --git a/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx b/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx index 5bc51d48..522e8bf4 100644 --- a/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx @@ -10,9 +10,9 @@ import { } from "../../../../store/editor/actionCreators"; import {AppState} from "../../../../store"; import {connect} from "react-redux"; -import * as _ from "lodash"; import LabelInputField from "../LabelInputField/LabelInputField"; import EmptyLabelList from "../EmptyLabelList/EmptyLabelList"; +import {LabelActions} from "../../../../logic/actions/LabelActions"; interface IProps { size: ISize; @@ -38,13 +38,7 @@ const PointLabelsList: React.FC = ({size, imageData, updateImageDataById }; const deletePointLabelById = (labelPointId: string) => { - const newImageData = { - ...imageData, - labelPoints: _.filter(imageData.labelPoints, (currentLabel: LabelPoint) => { - return currentLabel.id !== labelPointId; - }) - }; - updateImageDataById(imageData.id, newImageData); + LabelActions.deletePointLabelById(imageData.id, labelPointId); }; const updatePointLabel = (labelPointId: string, labelNameIndex: number) => { diff --git a/src/views/EditorView/SideNavigationBar/PolygonLabelsList/PolygonLabelsList.tsx b/src/views/EditorView/SideNavigationBar/PolygonLabelsList/PolygonLabelsList.tsx index d8b21fee..e3fea0fc 100644 --- a/src/views/EditorView/SideNavigationBar/PolygonLabelsList/PolygonLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/PolygonLabelsList/PolygonLabelsList.tsx @@ -10,9 +10,9 @@ import { } from "../../../../store/editor/actionCreators"; import {AppState} from "../../../../store"; import {connect} from "react-redux"; -import * as _ from "lodash"; import LabelInputField from "../LabelInputField/LabelInputField"; import EmptyLabelList from "../EmptyLabelList/EmptyLabelList"; +import {LabelActions} from "../../../../logic/actions/LabelActions"; interface IProps { size: ISize; @@ -38,13 +38,7 @@ const PolygonLabelsList: React.FC = ({size, imageData, updateImageDataBy }; const deletePolygonLabelById = (labelPolygonId: string) => { - const newImageData = { - ...imageData, - labelPolygons: _.filter(imageData.labelPolygons, (currentLabel: LabelPolygon) => { - return currentLabel.id !== labelPolygonId; - }) - }; - updateImageDataById(imageData.id, newImageData); + LabelActions.deletePolygonLabelById(imageData.id, labelPolygonId); }; const updatePolygonLabel = (labelPolygonId: string, labelNameIndex: number) => { diff --git a/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx b/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx index 904e2c7c..1091809e 100644 --- a/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx @@ -10,9 +10,9 @@ import { } from "../../../../store/editor/actionCreators"; import {AppState} from "../../../../store"; import {connect} from "react-redux"; -import * as _ from "lodash"; import LabelInputField from "../LabelInputField/LabelInputField"; import EmptyLabelList from "../EmptyLabelList/EmptyLabelList"; +import {LabelActions} from "../../../../logic/actions/LabelActions"; interface IProps { size: ISize; @@ -38,13 +38,7 @@ const RectLabelsList: React.FC = ({size, imageData, updateImageDataById, }; const deleteRectLabelById = (labelRectId: string) => { - const newImageData = { - ...imageData, - labelRects: _.filter(imageData.labelRects, (currentLabel: LabelRect) => { - return currentLabel.id !== labelRectId; - }) - }; - updateImageDataById(imageData.id, newImageData); + LabelActions.deleteRectLabelById(imageData.id, labelRectId); }; const updateRectLabel = (labelRectId: string, labelNameIndex: number) => { diff --git a/src/views/EditorView/SideNavigationBar/SideNavigationBar.scss b/src/views/EditorView/SideNavigationBar/SideNavigationBar.scss index 1475dd54..7f387ad5 100644 --- a/src/views/EditorView/SideNavigationBar/SideNavigationBar.scss +++ b/src/views/EditorView/SideNavigationBar/SideNavigationBar.scss @@ -10,6 +10,12 @@ align-items: center; align-content: flex-start; + &.with-context { + .NavigationBarContentWrapper { + background-color: $darkThemeForthColor; + } + } + .CompanionBar { align-self: stretch; min-width: $sideNavigationBarCompanionWidth; @@ -40,6 +46,10 @@ .CompanionBar { border-right: solid 1px $darkThemeFirstColor; + + .VerticalEditorButton { + transform: rotate(-90deg) translateY(-2px); + } } } @@ -49,6 +59,10 @@ .CompanionBar { border-left: solid 1px $darkThemeFirstColor; + + .VerticalEditorButton { + transform: rotate(-90deg) translateY(-1px); + } } } diff --git a/src/views/EditorView/SideNavigationBar/SideNavigationBar.tsx b/src/views/EditorView/SideNavigationBar/SideNavigationBar.tsx index 8617dadc..e8de5c7e 100644 --- a/src/views/EditorView/SideNavigationBar/SideNavigationBar.tsx +++ b/src/views/EditorView/SideNavigationBar/SideNavigationBar.tsx @@ -6,12 +6,13 @@ import {Direction} from "../../../data/enums/Direction"; interface IProps { direction: Direction isOpen: boolean; + isWithContext?: boolean; renderCompanion?: () => any; renderContent?: () => any; } export const SideNavigationBar: React.FC = (props) => { - const {direction, isOpen, renderContent, renderCompanion} = props; + const {direction, isOpen, isWithContext, renderContent, renderCompanion} = props; const getClassName = () => { return classNames( @@ -19,6 +20,7 @@ export const SideNavigationBar: React.FC = (props) => { { "left": direction === Direction.LEFT, "right": direction === Direction.RIGHT, + "with-context": isWithContext, "closed": !isOpen } ); diff --git a/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.scss b/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.scss new file mode 100644 index 00000000..71693cda --- /dev/null +++ b/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.scss @@ -0,0 +1,19 @@ +@import '../../../../settings/Settings'; + +.ImageButtonDropDownContent { + position: fixed; + z-index: 1000; + border-radius: 3px; + background-color: $darkThemeSecondColor; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + + &.with-context { + background-color: $darkThemeForthColor; + } +} \ No newline at end of file diff --git a/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.tsx b/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.tsx new file mode 100644 index 00000000..e230c43f --- /dev/null +++ b/src/views/EditorView/ToolBox/ImageButtonDropDown/ImageButtonDropDown.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import {ImageButtonDropDownData} from "../../../../data/ImageButtonDropDownData"; +import {ImageButton} from "../../../Common/ImageButton/ImageButton"; +import './ImageButtonDropDown.scss'; +import {AppState} from "../../../../store"; +import {connect} from "react-redux"; +import {updatePreventCustomCursorStatus} from "../../../../store/general/actionCreators"; +import classNames from "classnames"; +import {ContextType} from "../../../../data/enums/ContextType"; + +interface IProps { + coverData: ImageButtonDropDownData; + contentData?: ImageButtonDropDownData[]; + updatePreventCustomCursorStatus: (preventCustomCursor: boolean) => any; + activeContext: ContextType; +} + +interface IState { + isOpened: boolean; +} + +class ImageButtonDropDown extends React.Component { + private coverButton; + private contentWrapper: HTMLDivElement; + + constructor(props) { + super(props); + + this.state = { + isOpened: false, + } + } + public componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + if (!this.state.isOpened) return null; + + const coverClientRect: ClientRect | DOMRect = this.coverButton.getBoundingClientRect(); + this.contentWrapper.style.left = (coverClientRect.left + coverClientRect.width + 10) + "px"; + } + + private onCoverClickHandler = () => { + this.setState({isOpened: !this.state.isOpened}) + }; + + private onContentMouseEnter = () => { + this.props.updatePreventCustomCursorStatus(true); + }; + + private onContentMouseLeave = () => { + this.props.updatePreventCustomCursorStatus(false); + }; + + private getClassName = () => { + return classNames( + "ImageButtonDropDownContent", + { + "with-context": this.props.activeContext === ContextType.EDITOR + } + ); + }; + + private getContent = () => { + if (!this.state.isOpened) return null; + + const coverClientRect: ClientRect | DOMRect = this.coverButton.getBoundingClientRect(); + const style: React.CSSProperties = { + top: coverClientRect.top, + left: coverClientRect.left + coverClientRect.width + 10, + height: coverClientRect.height + }; + + return
this.contentWrapper = ref} + style={style} + onMouseEnter={this.onContentMouseEnter} + onMouseLeave={this.onContentMouseLeave} + > + {this.props.contentData.map((data: ImageButtonDropDownData, index: number) => { + return + })} +
+ }; + + render() { + return <> + this.coverButton = ref} + onClick={this.onCoverClickHandler} + isActive={this.state.isOpened} + /> + {this.getContent()} + + } +} + +const mapDispatchToProps = { + updatePreventCustomCursorStatus +}; + +const mapStateToProps = (state: AppState) => ({ + activeContext: state.general.activeContext +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ImageButtonDropDown); \ No newline at end of file diff --git a/src/views/EditorView/ToolBox/ToolBox.scss b/src/views/EditorView/ToolBox/ToolBox.scss new file mode 100644 index 00000000..70214260 --- /dev/null +++ b/src/views/EditorView/ToolBox/ToolBox.scss @@ -0,0 +1,39 @@ +@import '../../../settings/Settings'; + +.ToolBox { + align-self: stretch; + width: $toolboxWidth; + + border-right: solid 1px $darkThemeFirstColor; + background-color: $darkThemeSecondColor; + + &.with-context { + background-color: $darkThemeForthColor; + } + + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + padding: 3px 0; + + .ImageButton { + transition: transform 0.3s; + + img { + filter: brightness(0) invert(1); + } + + &:hover { + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.1); + } + + &.active { + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.1); + } + } +} \ No newline at end of file diff --git a/src/views/EditorView/ToolBox/ToolBox.tsx b/src/views/EditorView/ToolBox/ToolBox.tsx new file mode 100644 index 00000000..f8fd8d7d --- /dev/null +++ b/src/views/EditorView/ToolBox/ToolBox.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import './ToolBox.scss'; +import classNames from "classnames"; +import {ContextType} from "../../../data/enums/ContextType"; +import {AppState} from "../../../store"; +import {connect} from "react-redux"; +import {ImageButton} from "../../Common/ImageButton/ImageButton"; +import {ImageButtonDropDownData} from "../../../data/ImageButtonDropDownData"; +import {ViewPortActions} from "../../../logic/actions/ViewPortActions"; +import {ISize} from "../../../interfaces/ISize"; +import {updateImageDragModeStatus} from "../../../store/general/actionCreators"; +import ImageButtonDropDown from "./ImageButtonDropDown/ImageButtonDropDown"; +import {EditorModel} from "../../../staticModels/EditorModel"; +import {ViewPointSettings} from "../../../settings/ViewPointSettings"; + +interface IProps { + activeContext: ContextType; + size: ISize; + updateImageDragModeStatus: (imageDragMode: boolean) => any; + imageDragMode: boolean; +} + +const ToolBox: React.FC = ({activeContext, updateImageDragModeStatus, imageDragMode}) => { + + const zoomDropDownContentData: ImageButtonDropDownData[] = [ + { + image: "ico/zoom-in.png", + imageAlt: "zoom-in", + onClick: () => ViewPortActions.zoomIn() + }, + { + image: "ico/zoom-out.png", + imageAlt: "zoom-out", + onClick: () => ViewPortActions.zoomOut() + }, + { + image: "ico/zoom-fit.png", + imageAlt: "zoom-fit", + onClick: () => ViewPortActions.setDefaultZoom() + }, + { + image: "ico/zoom-max.png", + imageAlt: "zoom-max", + onClick: () => ViewPortActions.setOneForOneZoom() + } + ]; + + const imageDragOnClick = () => { + if (EditorModel.zoom !== ViewPointSettings.MIN_ZOOM) { + updateImageDragModeStatus(!imageDragMode); + } + }; + + const getClassName = () => { + return classNames( + "ToolBox", + { + "with-context": activeContext === ContextType.EDITOR + } + ); + }; + + return
+ + +
+}; + +const mapDispatchToProps = { + updateImageDragModeStatus +}; + +const mapStateToProps = (state: AppState) => ({ + activeContext: state.general.activeContext, + imageDragMode: state.general.imageDragMode +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ToolBox); \ No newline at end of file diff --git a/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx b/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx index 34a63062..d5aad8d1 100644 --- a/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx +++ b/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx @@ -70,7 +70,7 @@ const TopNavigationBar: React.FC = ({updateActivePopupType, updateProjec
diff --git a/src/views/MainView/MainView.tsx b/src/views/MainView/MainView.tsx index 0746e98e..048fac18 100644 --- a/src/views/MainView/MainView.tsx +++ b/src/views/MainView/MainView.tsx @@ -55,7 +55,7 @@ const MainView: React.FC = () => { >
= ({size}) => { return SocialMediaData.map((data:ISocialMedia, index: number) => { return any; updateProjectType: (projectType: ProjectType) => any; updateActiveLabelNameIndex: (activeLabelIndex: number) => any; updateLabelNamesList: (labelNames: string[]) => any; - updateActivePopupType: (activePopupType: PopupWindowType) => any; updateImageData: (imageData: ImageData[]) => any; updateFirstLabelCreatedFlag: (firstLabelCreatedFlag: boolean) => any; } @@ -30,7 +28,6 @@ const ExitProjectPopup: React.FC = (props) => { const { updateActiveLabelNameIndex, updateLabelNamesList, - updateActivePopupType, updateProjectType, updateActiveImageIndex, updateImageData, @@ -52,13 +49,13 @@ const ExitProjectPopup: React.FC = (props) => { updateLabelNamesList([]); updateProjectType(null); updateActiveImageIndex(null); - updateActivePopupType(null); updateImageData([]); updateFirstLabelCreatedFlag(false); + PopupActions.close(); }; const onReject = () => { - updateActivePopupType(null); + PopupActions.close(); }; return( @@ -75,7 +72,6 @@ const ExitProjectPopup: React.FC = (props) => { const mapDispatchToProps = { updateActiveLabelNameIndex, updateLabelNamesList, - updateActivePopupType, updateProjectType, updateActiveImageIndex, updateImageData, diff --git a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx index f3e197ac..b36b0a94 100644 --- a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx +++ b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx @@ -1,11 +1,8 @@ import React, {useState} from 'react' import './ExportLabelPopup.scss' -import {ImageData} from "../../../store/editor/types"; import {GenericYesNoPopup} from "../GenericYesNoPopup/GenericYesNoPopup"; import {AppState} from "../../../store"; import {connect} from "react-redux"; -import {PopupWindowType} from "../../../data/enums/PopupWindowType"; -import {updateActivePopupType} from "../../../store/general/actionCreators"; import {ExportFormatType} from "../../../data/enums/ExportFormatType"; import {RectLabelsExporter} from "../../../logic/export/RectLabelsExporter"; import {LabelType} from "../../../data/enums/LabelType"; @@ -16,13 +13,9 @@ import {PointExportFormatData} from "../../../data/export/PointExportFormatData" import {PointLabelsExporter} from "../../../logic/export/PointLabelsExport"; import {PolygonExportFormatData} from "../../../data/export/PolygonExportFormatData"; import {PolygonLabelsExporter} from "../../../logic/export/PolygonLabelsExporter"; +import {PopupActions} from "../../../logic/actions/PopupActions"; -interface IProps { - imagesData: ImageData[], - updateActivePopupType: (activePopupType: PopupWindowType) => any; -} - -const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) => { +const ExportLabelPopup: React.FC = () => { const [exportLabelType, setExportLabelType] = useState(LabelType.RECTANGLE); const [exportFormatType, setExportFormatType] = useState(null); @@ -39,11 +32,11 @@ const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) PolygonLabelsExporter.export(exportFormatType); break; } - updateActivePopupType(null); + PopupActions.close(); }; const onReject = () => { - updateActivePopupType(null); + PopupActions.close(); }; const onSelect = (exportFormatType: ExportFormatType) => { @@ -79,7 +72,7 @@ const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) { setExportLabelType(LabelType.RECTANGLE); @@ -90,7 +83,7 @@ const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) { setExportLabelType(LabelType.POINT); @@ -101,7 +94,7 @@ const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) { setExportLabelType(LabelType.POLYGON); @@ -135,9 +128,7 @@ const ExportLabelPopup: React.FC = ({imagesData, updateActivePopupType}) ); }; -const mapDispatchToProps = { - updateActivePopupType, -}; +const mapDispatchToProps = {}; const mapStateToProps = (state: AppState) => ({ imagesData: state.editor.imagesData diff --git a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx index e7a8a84a..40249a64 100644 --- a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx +++ b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx @@ -10,6 +10,7 @@ import Scrollbars from 'react-custom-scrollbars'; import TextInput from "../../Common/TextInput/TextInput"; import {ImageButton} from "../../Common/ImageButton/ImageButton"; import uuidv1 from 'uuid/v1'; +import {PopupActions} from "../../../logic/actions/PopupActions"; interface IProps { updateActiveLabelNameIndex: (activeLabelIndex: number) => any; @@ -37,12 +38,12 @@ const InsertLabelNamesPopup: React.FC = ({updateActiveLabelNameIndex, up key={key} isPassword={false} onChange={(event: React.ChangeEvent) => onChange(key, event.target.value)} - label={"Inset label"} + label={"Insert label"} /> deleteHandle(key)} />
@@ -55,8 +56,10 @@ const InsertLabelNamesPopup: React.FC = ({updateActiveLabelNameIndex, up const onAccept = () => { const labelNamesList: string[] = extractLabelNamesList(); - updateLabelNamesList(labelNamesList); - updateActivePopupType(null); + if (labelNamesList.length > 0) { + updateLabelNamesList(labelNamesList); + PopupActions.close(); + } }; const extractLabelNamesList = (): string[] => { @@ -73,7 +76,7 @@ const InsertLabelNamesPopup: React.FC = ({updateActiveLabelNameIndex, up @@ -130,4 +133,4 @@ const mapStateToProps = (state: AppState) => ({}); export default connect( mapStateToProps, mapDispatchToProps -)(InsertLabelNamesPopup); \ No newline at end of file +)(InsertLabelNamesPopup); diff --git a/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx b/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx index 864de41b..46abd1ec 100644 --- a/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx +++ b/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx @@ -9,6 +9,7 @@ import {updateActivePopupType} from "../../../store/general/actionCreators"; import {useDropzone} from "react-dropzone"; import {FileUtil} from "../../../utils/FileUtil"; import {AcceptedFileType} from "../../../data/enums/AcceptedFileType"; +import {PopupActions} from "../../../logic/actions/PopupActions"; interface IProps { updateActiveLabelNameIndex: (activeLabelIndex: number) => any; @@ -43,7 +44,7 @@ const LoadLabelNamesPopup: React.FC = ({updateActiveLabelNameIndex, upda if (labelsList.length > 0) { updateActiveLabelNameIndex(0); updateLabelNamesList(labelsList); - updateActivePopupType(null); + PopupActions.close(); } }; diff --git a/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.tsx b/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.tsx index fe76694f..de60324d 100644 --- a/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.tsx +++ b/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.tsx @@ -4,19 +4,17 @@ import {AppState} from "../../../store"; import {connect} from "react-redux"; import {addImageData} from "../../../store/editor/actionCreators"; import {GenericYesNoPopup} from "../GenericYesNoPopup/GenericYesNoPopup"; -import {PopupWindowType} from "../../../data/enums/PopupWindowType"; -import {updateActivePopupType} from "../../../store/general/actionCreators"; import {useDropzone} from "react-dropzone"; import {FileUtil} from "../../../utils/FileUtil"; import {ImageData} from "../../../store/editor/types"; import {AcceptedFileType} from "../../../data/enums/AcceptedFileType"; +import {PopupActions} from "../../../logic/actions/PopupActions"; interface IProps { - updateActivePopupType: (activePopupType: PopupWindowType) => any; addImageData: (imageData: ImageData[]) => any; } -const LoadMoreImagesPopup: React.FC = ({updateActivePopupType, addImageData}) => { +const LoadMoreImagesPopup: React.FC = ({addImageData}) => { const {acceptedFiles, getRootProps, getInputProps} = useDropzone({ accept: AcceptedFileType.IMAGE }); @@ -24,12 +22,12 @@ const LoadMoreImagesPopup: React.FC = ({updateActivePopupType, addImageD const onAccept = () => { if (acceptedFiles.length > 0) { addImageData(acceptedFiles.map((fileData:File) => FileUtil.mapFileDataToImageData(fileData))); - updateActivePopupType(null); + PopupActions.close(); } }; const onReject = () => { - updateActivePopupType(null); + PopupActions.close(); }; const getDropZoneContent = () => { @@ -88,7 +86,6 @@ const LoadMoreImagesPopup: React.FC = ({updateActivePopupType, addImageD }; const mapDispatchToProps = { - updateActivePopupType, addImageData };