From 5e09f67ea108d7bd88402c963403c39b04c024fc Mon Sep 17 00:00:00 2001 From: Piotr Skalski Date: Fri, 16 Aug 2019 00:15:59 +0200 Subject: [PATCH] 1.1.0-alpha relese merge (#20) * Adding support for rect export to Pascal VOC XML format * Adding snapping points and rectangles when the mouse is outside the outline of the photo during active labeling --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++ .github/ISSUE_TEMPLATE/custom.md | 10 ++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .idea/inspectionProfiles/Project_Default.xml | 6 - .idea/vcs.xml | 6 - .idea/watcherTasks.xml | 4 - README.md | 147 ++++++++++++++++-- public/ico/minus.png | Bin 132 -> 0 bytes public/ico/plus.png | Bin 304 -> 179 bytes src/data/EditorFeatureData.ts | 2 +- src/data/ExportFormatType.ts | 3 +- src/data/RectExportFormatData.ts | 4 + src/logic/export/PointLabelsExport.ts | 14 +- src/logic/export/RectLabelsExporter.ts | 137 ++++++++++++++-- src/logic/render/BaseSuportRenderEngine.ts | 10 ++ src/logic/render/PointRenderEngine.ts | 39 +++-- src/logic/render/PolygonRenderEngine.ts | 8 +- src/logic/render/RectRenderEngine.ts | 121 +++++++++----- src/settings/Settings.ts | 2 +- src/store/Actions.ts | 1 + src/store/editor/actionCreators.ts | 9 ++ src/store/editor/reducer.ts | 7 + src/store/editor/types.ts | 9 ++ src/store/selectors/EditorSelector.ts | 16 ++ src/utils/CanvasUtil.ts | 26 ++++ src/utils/NumberUtil.ts | 10 ++ src/utils/RectUtil.ts | 9 ++ src/utils/XMLSanitizerUtil.ts | 10 ++ src/views/Common/ImageButton/ImageButton.scss | 4 + src/views/Common/TextInput/TextInput.tsx | 12 +- src/views/EditorView/Editor/Editor.tsx | 38 +++-- .../ImagePreview/ImagePreview.scss | 1 + .../LabelInputField/LabelInputField.scss | 4 + .../LabelInputField/LabelInputField.tsx | 1 + .../TopNavigationBar/TopNavigationBar.scss | 32 ++++ .../TopNavigationBar/TopNavigationBar.tsx | 41 +++-- .../ExportLabelsPopup/ExportLabelPopup.scss | 10 +- .../InsertLabelNamesPopup.scss | 10 +- .../InsertLabelNamesPopup.tsx | 3 +- 39 files changed, 689 insertions(+), 135 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/watcherTasks.xml delete mode 100644 public/ico/minus.png create mode 100644 src/logic/render/BaseSuportRenderEngine.ts create mode 100644 src/store/selectors/EditorSelector.ts create mode 100644 src/utils/NumberUtil.ts create mode 100644 src/utils/XMLSanitizerUtil.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 03d9549e..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml deleted file mode 100644 index fb0d65a4..00000000 --- a/.idea/watcherTasks.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/README.md b/README.md index a47a8d86..e90d1b20 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,19 @@ Our application is being actively developed. If you have an idea for a new functionality, please hit us on [Twitter][3] or create an issue where you can describe your concept. In the meantime, see what improvements we are planning for you in the future. -* Optimization of the process of loading photos from disk - queuing -* Labelling objects using polygons and Bézier curves -* Export labels in COCO JSON format -* 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 +- [X] Export labels in Pascal VOC XML format +- [ ] Optimization of the process of loading photos from disk - queuing +- [ ] Labelling objects using polygons and Bézier curves +- [ ] Labelling objects using lines +- [ ] Export labels in COCO JSON format +- [ ] 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 ## Sneak Peek @@ -43,18 +48,28 @@ npm install npm start ``` +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]. + ## Supported Output Formats * 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 -`rel_rect_center_y` - vertical position of the centre of the rect in relation to overall image height -`rel_rect_width` - rect width in relation to overall image width -`rel_rect_height` - rect height in relation to overall image height +`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 @@ -62,11 +77,114 @@ npm start ```

+* 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

- -`label_name,rect_left,rect_top,rect_width,rect_height,image_name,image_width,image_height` + +**Schema:** + +`label_name,rect_left,rect_top,rect_width,rect_height,image_name,image_width,image_height` + +**Where:** + +`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 + +**Example:** ``` banana,491,164,530,614,000000.jpg,1280,960 @@ -92,3 +210,4 @@ Copyright (c) 2019-present, Piotr Skalski [1]: http://makesense.ai [2]: ./LICENSE [3]: https://twitter.com/PiotrSkalski92 +[4]: https://github.com/SkalskiP/make-sense/issues/16 diff --git a/public/ico/minus.png b/public/ico/minus.png deleted file mode 100644 index 1232daeec6239c5447507c8e8347a88f7fc84e5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYuI>dsK<0d{mOvoISQ6wH z%;50sMjDW#vf`r$B4SQll+NV@hRtoa1EK-n|x@h^7L`yaS z*CWDK8cs$WvKPF#`i+GhX;-ou@)?pPgR5mV7RMEG1I{M_BlDr?=E@Z4D@1!?1!IW2U%Hw3Pl%e oHI)4Khsnct?yWT)9WTCx#WXi1Z;$^O2sDPl)78&qol`;+0P#>;umAu6 diff --git a/src/data/EditorFeatureData.ts b/src/data/EditorFeatureData.ts index a5e502d7..17ff5632 100644 --- a/src/data/EditorFeatureData.ts +++ b/src/data/EditorFeatureData.ts @@ -26,7 +26,7 @@ export const EditorFeatureData: IEditorFeature[] = [ imageAlt: "labels", }, { - displayText: "Support output file formats like YOLO, CSV", + displayText: "Support output file formats like YOLO, VOC XML, CSV", imageSrc: "img/file.png", imageAlt: "file", }, diff --git a/src/data/ExportFormatType.ts b/src/data/ExportFormatType.ts index 53d713cf..48614e34 100644 --- a/src/data/ExportFormatType.ts +++ b/src/data/ExportFormatType.ts @@ -1,5 +1,6 @@ export enum ExportFormatType { YOLO = "YOLO", COCO = "COCO", - CSV = "CSV" + CSV = "CSV", + VOC = "VOC" } \ No newline at end of file diff --git a/src/data/RectExportFormatData.ts b/src/data/RectExportFormatData.ts index 91675702..1b814222 100644 --- a/src/data/RectExportFormatData.ts +++ b/src/data/RectExportFormatData.ts @@ -6,6 +6,10 @@ export const RectExportFormatData: IExportFormat[] = [ type: ExportFormatType.YOLO, label: "A .zip package containing files in YOLO format." }, + { + type: ExportFormatType.VOC, + label: "A .zip package containing files in VOC XML format." + }, { type: ExportFormatType.CSV, label: "Single CSV file." diff --git a/src/logic/export/PointLabelsExport.ts b/src/logic/export/PointLabelsExport.ts index 059f7d85..5fd87845 100644 --- a/src/logic/export/PointLabelsExport.ts +++ b/src/logic/export/PointLabelsExport.ts @@ -1,9 +1,9 @@ import {ExportFormatType} from "../../data/ExportFormatType"; -import {store} from "../../index"; import {ImageData, LabelPoint} from "../../store/editor/types"; import {saveAs} from "file-saver"; import {ImageRepository} from "../imageRepository/ImageRepository"; import moment from 'moment'; +import {EditorSelector} from "../../store/selectors/EditorSelector"; export class PointLabelsExporter { public static export(exportFormatType: ExportFormatType): void { @@ -17,16 +17,22 @@ export class PointLabelsExporter { } private static exportAsCSV(): void { - const content: string = store.getState().editor.imagesData + const content: string = EditorSelector.getImagesData() .map((imageData: ImageData) => { return PointLabelsExporter.wrapRectLabelsIntoCSV(imageData)}) .filter((imageLabelData: string) => { return !!imageLabelData}) .join("\n"); + const projectName: string = EditorSelector.getProjectName(); const date: string = moment().format('YYYYMMDDhhmmss'); const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); - saveAs(blob, "labels_" + date + ".csv"); + try { + saveAs(blob, `labels_${projectName}_${date}.csv`); + } catch (error) { + // TODO + throw new Error(error); + } } private static wrapRectLabelsIntoCSV(imageData: ImageData): string { @@ -34,7 +40,7 @@ export class PointLabelsExporter { return null; const image: HTMLImageElement = ImageRepository.getById(imageData.id); - const labelNamesList: string[] = store.getState().editor.labelNames; + const labelNamesList: string[] = EditorSelector.getLabelNames(); const labelRectsString: string[] = imageData.labelPoints.map((labelPoint: LabelPoint) => { const labelFields = [ labelNamesList[labelPoint.labelIndex], diff --git a/src/logic/export/RectLabelsExporter.ts b/src/logic/export/RectLabelsExporter.ts index f9e4d3be..71460d3c 100644 --- a/src/logic/export/RectLabelsExporter.ts +++ b/src/logic/export/RectLabelsExporter.ts @@ -1,10 +1,11 @@ import {ExportFormatType} from "../../data/ExportFormatType"; import {ImageData, LabelRect} from "../../store/editor/types"; import {ImageRepository} from "../imageRepository/ImageRepository"; -import {store} from "../.."; import JSZip from 'jszip'; import { saveAs } from 'file-saver'; import moment from 'moment'; +import {EditorSelector} from "../../store/selectors/EditorSelector"; +import {XMLSanitizerUtil} from "../../utils/XMLSanitizerUtil"; export class RectLabelsExporter { public static export(exportFormatType: ExportFormatType): void { @@ -12,6 +13,9 @@ export class RectLabelsExporter { case ExportFormatType.YOLO: RectLabelsExporter.exportAsYOLO(); break; + case ExportFormatType.VOC: + RectLabelsExporter.exportAsVOC(); + break; case ExportFormatType.CSV: RectLabelsExporter.exportAsCSV(); break; @@ -22,18 +26,33 @@ export class RectLabelsExporter { private static exportAsYOLO(): void { let zip = new JSZip(); - store.getState().editor.imagesData.forEach((imageData: ImageData) => { - const fileContent: string = RectLabelsExporter.wrapRectLabelsIntoYOLO(imageData); - if (fileContent) { - const fileName : string = imageData.fileData.name.replace(/\.[^/.]+$/, ".txt"); - zip.file(fileName, fileContent); - } - }); - const date: string = moment().format('YYYYMMDDhhmmss'); - zip.generateAsync({type:"blob"}) - .then(function(content) { - saveAs(content, "labels_yolo_" + date + ".zip"); + EditorSelector.getImagesData() + .forEach((imageData: ImageData) => { + const fileContent: string = RectLabelsExporter.wrapRectLabelsIntoYOLO(imageData); + if (fileContent) { + const fileName : string = imageData.fileData.name.replace(/\.[^/.]+$/, ".txt"); + try { + zip.file(fileName, fileContent); + } catch (error) { + // TODO + throw new Error(error); + } + } }); + + const projectName: string = EditorSelector.getProjectName(); + const date: string = moment().format('YYYYMMDDhhmmss'); + + try { + zip.generateAsync({type:"blob"}) + .then(function(content) { + saveAs(content, `labels_${projectName}_${date}.zip`); + }); + } catch (error) { + // TODO + throw new Error(error); + } + } private static wrapRectLabelsIntoYOLO(imageData: ImageData): string { @@ -54,17 +73,105 @@ export class RectLabelsExporter { return labelRectsString.join("\n"); } + private static exportAsVOC(): void { + let zip = new JSZip(); + EditorSelector.getImagesData().forEach((imageData: ImageData) => { + const fileContent: string = RectLabelsExporter.wrapImageIntoVOC(imageData); + if (fileContent) { + const fileName : string = imageData.fileData.name.replace(/\.[^/.]+$/, ".xml"); + try { + zip.file(fileName, fileContent); + } catch (error) { + // TODO + throw new Error(error); + } + } + }); + + const projectName: string = EditorSelector.getProjectName(); + const date: string = moment().format('YYYYMMDDhhmmss'); + + try { + zip.generateAsync({type:"blob"}) + .then(function(content) { + saveAs(content, `labels_${projectName}_${date}.zip`); + }); + } catch (error) { + // TODO + throw new Error(error); + } + } + + private static wrapRectLabelsIntoVOC(imageData: ImageData): string { + if (imageData.labelRects.length === 0 || !imageData.loadStatus) + return null; + + const labelNamesList: string[] = EditorSelector.getLabelNames(); + const labelRectsString: string[] = imageData.labelRects.map((labelRect: LabelRect) => { + const labelFields = [ + `\t`, + `\t\t${labelNamesList[labelRect.labelIndex]}`, + `\t\tUnspecified`, + `\t\tUnspecified`, + `\t\tUnspecified`, + `\t\t`, + `\t\t\t${Math.round(labelRect.rect.x)}`, + `\t\t\t${Math.round(labelRect.rect.y)}`, + `\t\t\t${Math.round(labelRect.rect.x + labelRect.rect.width)}`, + `\t\t\t${Math.round(labelRect.rect.y + labelRect.rect.height)}`, + `\t\t`, + `\t` + ]; + return labelFields.join("\n") + }); + return labelRectsString.join("\n"); + } + + private static wrapImageIntoVOC(imageData: ImageData): string { + const labels: string = RectLabelsExporter.wrapRectLabelsIntoVOC(imageData); + const projectName: string = XMLSanitizerUtil.sanitize(EditorSelector.getProjectName()); + + if (labels) { + const image: HTMLImageElement = ImageRepository.getById(imageData.id); + return [ + ``, + `\t${projectName}`, + `\t${imageData.fileData.name}`, + `\t/${projectName}/${imageData.fileData.name}`, + `\t`, + `\t\tUnspecified`, + `\t`, + `\t`, + `\t\t${image.width}`, + `\t\t${image.height}`, + `\t\t3`, + `\t`, + labels, + `` + ].join("\n"); + } + return null; + } + + private static exportAsCSV(): void { - const content: string = store.getState().editor.imagesData + const content: string = EditorSelector.getImagesData() .map((imageData: ImageData) => { return RectLabelsExporter.wrapRectLabelsIntoCSV(imageData)}) .filter((imageLabelData: string) => { return !!imageLabelData}) .join("\n"); + const projectName: string = EditorSelector.getProjectName(); const date: string = moment().format('YYYYMMDDhhmmss'); const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); - saveAs(blob, "labels_" + date + ".csv"); + + try { + saveAs(blob, `labels_${projectName}_${date}.csv`); + } catch (error) { + // TODO + throw new Error(error); + } } private static wrapRectLabelsIntoCSV(imageData: ImageData): string { @@ -72,7 +179,7 @@ export class RectLabelsExporter { return null; const image: HTMLImageElement = ImageRepository.getById(imageData.id); - const labelNamesList: string[] = store.getState().editor.labelNames; + const labelNamesList: string[] = EditorSelector.getLabelNames(); const labelRectsString: string[] = imageData.labelRects.map((labelRect: LabelRect) => { const labelFields = [ labelNamesList[labelRect.labelIndex], diff --git a/src/logic/render/BaseSuportRenderEngine.ts b/src/logic/render/BaseSuportRenderEngine.ts new file mode 100644 index 00000000..5d2f905d --- /dev/null +++ b/src/logic/render/BaseSuportRenderEngine.ts @@ -0,0 +1,10 @@ +import {IRect} from "../../interfaces/IRect"; +import {BaseRenderEngine} from "./BaseRenderEngine"; + +export abstract class BaseSuportRenderEngine extends BaseRenderEngine{ + public constructor(canvas: HTMLCanvasElement, imageRect: IRect) { + super(canvas, imageRect); + } + + abstract isInProgress(): boolean; +} \ No newline at end of file diff --git a/src/logic/render/PointRenderEngine.ts b/src/logic/render/PointRenderEngine.ts index ce99ee3e..35b23c3c 100644 --- a/src/logic/render/PointRenderEngine.ts +++ b/src/logic/render/PointRenderEngine.ts @@ -1,4 +1,3 @@ -import {BaseRenderEngine} from "./BaseRenderEngine"; import {IRect} from "../../interfaces/IRect"; import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; import {IPoint} from "../../interfaces/IPoint"; @@ -18,8 +17,10 @@ import {DrawUtil} from "../../utils/DrawUtil"; import {PointUtil} from "../../utils/PointUtil"; import {updateCustomcursorStyle} from "../../store/general/actionCreators"; import {CustomCursorStyle} from "../../data/CustomCursorStyle"; +import {BaseSuportRenderEngine} from "./BaseSuportRenderEngine"; +import {NumberUtil} from "../../utils/NumberUtil"; -export class PointRenderEngine extends BaseRenderEngine { +export class PointRenderEngine extends BaseSuportRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); // ================================================================================================================= @@ -41,7 +42,10 @@ export class PointRenderEngine extends BaseRenderEngine { public mouseDownHandler(event: MouseEvent): void { const mousePosition: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); const isMouseOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, mousePosition); - if (isMouseOverImage) { + const isMouseOverCanvas: boolean = RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, + this.mousePosition); + + if (isMouseOverCanvas) { const labelPoint: LabelPoint = this.getLabelPointUnderMouse(); if (!!labelPoint) { const pointOnImage: IPoint = this.calculatePointRelativeToActiveImage(labelPoint.point); @@ -61,7 +65,7 @@ export class PointRenderEngine extends BaseRenderEngine { }; this.addPointLabel(point); } - } else { + } else if (isMouseOverImage) { const scale = this.scale; const point: IPoint = { x: (mousePosition.x - this.imageRectOnCanvas.x) * scale, @@ -74,14 +78,14 @@ export class PointRenderEngine extends BaseRenderEngine { public mouseUpHandler(event: MouseEvent): void { const mousePosition: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); - const isOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, mousePosition); - if (isOverImage && this.transformInProgress) { + if (this.transformInProgress) { const scale = this.scale; const activeLabelPoint: LabelPoint = this.getActivePointLabel(); + const snappedPoint: IPoint = this.snapPointToImage(mousePosition); const scaledPoint: IPoint = PointRenderEngine.scalePoint({ - x: mousePosition.x - this.imageRectOnCanvas.x, - y: mousePosition.y - this.imageRectOnCanvas.y, + x: snappedPoint.x - this.imageRectOnCanvas.x, + y: snappedPoint.y - this.imageRectOnCanvas.y, }, scale); const imageData = this.getActiveImage(); @@ -128,7 +132,8 @@ export class PointRenderEngine extends BaseRenderEngine { imageData.labelPoints.forEach((labelPoint: LabelPoint) => { if (labelPoint.id === activeLabelId) { if (this.transformInProgress) { - const pointBetweenPixels = DrawUtil.setPointBetweenPixels(this.mousePosition); + const pointSnapped: IPoint = this.snapPointToImage(this.mousePosition); + const pointBetweenPixels: IPoint = DrawUtil.setPointBetweenPixels(pointSnapped); const handleRect: IRect = RectUtil.getRectWithCenterAndSize(pointBetweenPixels, this.config.anchorSize); DrawUtil.drawRectWithFill(this.canvas, handleRect, this.config.activeAnchorColor); } else { @@ -168,7 +173,7 @@ export class PointRenderEngine extends BaseRenderEngine { return; } - if (RectUtil.isPointInside(this.imageRectOnCanvas, this.mousePosition)) { + if (RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, this.mousePosition)) { store.dispatch(updateCustomcursorStyle(CustomCursorStyle.DEFAULT)); this.canvas.style.cursor = "none"; } else { @@ -186,6 +191,10 @@ export class PointRenderEngine extends BaseRenderEngine { this.scale = this.getActiveImageScale(); } + public isInProgress(): boolean { + return !!this.transformInProgress; + } + private static scalePoint(inputPoint:IPoint, scale: number): IPoint { return { x: inputPoint.x * scale, @@ -229,4 +238,14 @@ export class PointRenderEngine extends BaseRenderEngine { store.dispatch(updateFirstLabelCreatedFlag(true)); store.dispatch(updateActiveLabelId(labelPoint.id)); }; + + private snapPointToImage(point: IPoint): IPoint { + if (RectUtil.isPointInside(this.imageRectOnCanvas, point)) + return point; + + return { + x: NumberUtil.snapValueToRange(point.x, this.imageRectOnCanvas.x, this.imageRectOnCanvas.x + this.imageRectOnCanvas.width), + y: NumberUtil.snapValueToRange(point.y, this.imageRectOnCanvas.y, this.imageRectOnCanvas.y + this.imageRectOnCanvas.height) + } + } } \ No newline at end of file diff --git a/src/logic/render/PolygonRenderEngine.ts b/src/logic/render/PolygonRenderEngine.ts index 487e3b98..34382e37 100644 --- a/src/logic/render/PolygonRenderEngine.ts +++ b/src/logic/render/PolygonRenderEngine.ts @@ -1,4 +1,3 @@ -import {BaseRenderEngine} from "./BaseRenderEngine"; import {IRect} from "../../interfaces/IRect"; import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; import {IPoint} from "../../interfaces/IPoint"; @@ -7,8 +6,9 @@ import {store} from "../../index"; import {RectUtil} from "../../utils/RectUtil"; import {updateCustomcursorStyle} from "../../store/general/actionCreators"; import {CustomCursorStyle} from "../../data/CustomCursorStyle"; +import {BaseSuportRenderEngine} from "./BaseSuportRenderEngine"; -export class PolygonRenderEngine extends BaseRenderEngine { +export class PolygonRenderEngine extends BaseSuportRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); // ================================================================================================================= @@ -63,4 +63,8 @@ export class PolygonRenderEngine extends BaseRenderEngine { public updateImageRect(imageRect: IRect): void { this.imageRectOnCanvas = imageRect; } + + public isInProgress(): boolean { + return false; + } } \ No newline at end of file diff --git a/src/logic/render/RectRenderEngine.ts b/src/logic/render/RectRenderEngine.ts index 89d386ef..98a37a40 100644 --- a/src/logic/render/RectRenderEngine.ts +++ b/src/logic/render/RectRenderEngine.ts @@ -2,7 +2,6 @@ import {IPoint} from "../../interfaces/IPoint"; import {IRect} from "../../interfaces/IRect"; import {RectUtil} from "../../utils/RectUtil"; import {DrawUtil} from "../../utils/DrawUtil"; -import {BaseRenderEngine} from "./BaseRenderEngine"; import {store} from "../.."; import {ImageData, LabelRect} from "../../store/editor/types"; import uuidv1 from 'uuid/v1'; @@ -19,8 +18,10 @@ import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; import {CanvasUtil} from "../../utils/CanvasUtil"; import {updateCustomcursorStyle} from "../../store/general/actionCreators"; import {CustomCursorStyle} from "../../data/CustomCursorStyle"; +import {BaseSuportRenderEngine} from "./BaseSuportRenderEngine"; +import {NumberUtil} from "../../utils/NumberUtil"; -export class RectRenderEngine extends BaseRenderEngine { +export class RectRenderEngine extends BaseSuportRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); // ================================================================================================================= @@ -41,9 +42,12 @@ export class RectRenderEngine extends BaseRenderEngine { // ================================================================================================================= public mouseDownHandler = (event: MouseEvent) => { - const mousePosition: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); - const isMouseOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, mousePosition); - if (isMouseOverImage) { + this.mousePosition = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); + const isMouseOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, this.mousePosition); + const isMouseOverCanvas: boolean = RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, + this.mousePosition); + + if (isMouseOverCanvas) { const rectUnderMouse: LabelRect = this.getRectUnderMouse(); if (!!rectUnderMouse) { const rect: IRect = this.calculateRectRelativeToActiveImage(rectUnderMouse.rect); @@ -52,26 +56,26 @@ export class RectRenderEngine extends BaseRenderEngine { store.dispatch(updateActiveLabelId(rectUnderMouse.id)); this.startRectResize(anchorUnderMouse); } else { - this.startRectCreation(mousePosition); + this.startRectCreation(this.mousePosition); } - } else { - this.startRectCreation(mousePosition); + } else if (isMouseOverImage) { + this.startRectCreation(this.mousePosition); } } }; public mouseUpHandler = (event: MouseEvent) => { if (!!this.imageRectOnCanvas) { - const mousePosition: IPoint = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); - const isOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, mousePosition); + this.mousePosition = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); + const mousePositionSnapped: IPoint = this.snapPointToImage(this.mousePosition); - if (isOverImage && !!this.startCreateRectPoint && !PointUtil.equals(this.startCreateRectPoint, this.mousePosition)) { + if (!!this.startCreateRectPoint && !PointUtil.equals(this.startCreateRectPoint, mousePositionSnapped)) { const scale = this.scale; - const minX: number = Math.min(this.startCreateRectPoint.x, this.mousePosition.x); - const minY: number = Math.min(this.startCreateRectPoint.y, this.mousePosition.y); - const maxX: number = Math.max(this.startCreateRectPoint.x, this.mousePosition.x); - const maxY: number = Math.max(this.startCreateRectPoint.y, this.mousePosition.y); + const minX: number = Math.min(this.startCreateRectPoint.x, mousePositionSnapped.x); + const minY: number = Math.min(this.startCreateRectPoint.y, mousePositionSnapped.y); + 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 - this.imageRectOnCanvas.x) * scale, @@ -82,7 +86,7 @@ export class RectRenderEngine extends BaseRenderEngine { this.addRectLabel(rect); } - if (isOverImage && !!this.startResizeRectAnchor) { + if (!!this.startResizeRectAnchor) { const activeLabelRect: LabelRect = this.getActiveRectLabel(); const rect: IRect = this.calculateRectRelativeToActiveImage(activeLabelRect.rect); const startAnchorPosition = { @@ -90,12 +94,12 @@ export class RectRenderEngine extends BaseRenderEngine { y: this.startResizeRectAnchor.middlePosition.y + this.imageRectOnCanvas.y }; const delta = { - x: this.mousePosition.x - startAnchorPosition.x, - y: this.mousePosition.y - startAnchorPosition.y + x: mousePositionSnapped.x - startAnchorPosition.x, + y: mousePositionSnapped.y - startAnchorPosition.y }; - const resizedRect: IRect = RectUtil.resizeRect(rect, this.startResizeRectAnchor.type, delta); + const resizeRect: IRect = RectUtil.resizeRect(rect, this.startResizeRectAnchor.type, delta); const scale = this.scale; - const scaledRect: IRect = RectRenderEngine.scaleRect(resizedRect, scale); + const scaledRect: IRect = RectRenderEngine.scaleRect(resizeRect, scale); const imageData = this.getActiveImage(); imageData.labelRects = imageData.labelRects.map((labelRect: LabelRect) => { @@ -117,7 +121,7 @@ export class RectRenderEngine extends BaseRenderEngine { this.mousePosition = CanvasUtil.getMousePositionOnCanvasFromEvent(event, this.canvas); if (!!this.imageRectOnCanvas) { const isOverImage: boolean = RectUtil.isPointInside(this.imageRectOnCanvas, this.mousePosition); - if (isOverImage) { + if (isOverImage && !this.startResizeRectAnchor) { const labelRect: LabelRect = this.getRectUnderMouse(); if (!!labelRect) { if (store.getState().editor.highlightedLabelId !== labelRect.id) { @@ -151,11 +155,12 @@ export class RectRenderEngine extends BaseRenderEngine { private drawCurrentlyCreatedRect() { if (!!this.startCreateRectPoint) { + const mousePositionSnapped: IPoint = this.snapPointToImage(this.mousePosition); const activeRect: IRect = { x: this.startCreateRectPoint.x, y: this.startCreateRectPoint.y, - width: this.mousePosition.x - this.startCreateRectPoint.x, - height: this.mousePosition.y - this.startCreateRectPoint.y + width: mousePositionSnapped.x - this.startCreateRectPoint.x, + height: mousePositionSnapped.y - this.startCreateRectPoint.y }; const activeRectBetweenPixels = DrawUtil.setRectBetweenPixels(activeRect); DrawUtil.drawRect(this.canvas, activeRectBetweenPixels, this.config.rectActiveColor, this.config.rectThickness); @@ -171,13 +176,14 @@ export class RectRenderEngine extends BaseRenderEngine { private drawActiveRect(labelRect: LabelRect) { let rect: IRect = this.calculateRectRelativeToActiveImage(labelRect.rect); if (!!this.startResizeRectAnchor) { - const startAnchorPosition = { + const startAnchorPosition: IPoint = { x: this.startResizeRectAnchor.middlePosition.x + this.imageRectOnCanvas.x, y: this.startResizeRectAnchor.middlePosition.y + this.imageRectOnCanvas.y }; + const endAnchorPositionSnapped: IPoint = this.snapPointToImage(this.mousePosition); const delta = { - x: this.mousePosition.x - startAnchorPosition.x, - y: this.mousePosition.y - startAnchorPosition.y + x: endAnchorPositionSnapped.x - startAnchorPosition.x, + y: endAnchorPositionSnapped.y - startAnchorPosition.y }; rect = RectUtil.resizeRect(rect, this.startResizeRectAnchor.type, delta); } @@ -206,8 +212,12 @@ export class RectRenderEngine extends BaseRenderEngine { store.dispatch(updateCustomcursorStyle(CustomCursorStyle.MOVE)); return; } - if (RectUtil.isPointInside(this.imageRectOnCanvas, this.mousePosition)) { - store.dispatch(updateCustomcursorStyle(CustomCursorStyle.DEFAULT)); + if (RectUtil.isPointInside({x: 0, y: 0, ...CanvasUtil.getSize(this.canvas)}, this.mousePosition)) { + if (!RectUtil.isPointInside(this.imageRectOnCanvas, this.mousePosition) && !!this.startCreateRectPoint) + store.dispatch(updateCustomcursorStyle(CustomCursorStyle.MOVE)); + else + store.dispatch(updateCustomcursorStyle(CustomCursorStyle.DEFAULT)); + this.canvas.style.cursor = "none"; } else { this.canvas.style.cursor = "default"; @@ -219,6 +229,15 @@ export class RectRenderEngine extends BaseRenderEngine { // HELPERS // ================================================================================================================= + public updateImageRect(imageRect: IRect): void { + this.imageRectOnCanvas = imageRect; + this.scale = this.getActiveImageScale(); + } + + public isInProgress(): boolean { + return !!this.startCreateRectPoint || !!this.startResizeRectAnchor; + } + private static scaleRect(inputRect:IRect, scale: number): IRect { return { x: inputRect.x * scale, @@ -233,11 +252,6 @@ export class RectRenderEngine extends BaseRenderEngine { return RectRenderEngine.scaleRect(rect, 1/scale); } - public updateImageRect(imageRect: IRect): void { - this.imageRectOnCanvas = imageRect; - this.scale = this.getActiveImageScale(); - } - private addRectLabel = (rect: IRect) => { const activeImageIndex = store.getState().editor.activeImageIndex; const activeLabelIndex = store.getState().editor.activeLabelNameIndex; @@ -259,15 +273,40 @@ export class RectRenderEngine extends BaseRenderEngine { } private getRectUnderMouse(): LabelRect { + const activeRectLabel: LabelRect = this.getActiveRectLabel(); + if (!!activeRectLabel && this.isMouseOverRectEdges(activeRectLabel.rect)) { + return activeRectLabel; + } + const labelRects: LabelRect[] = this.getActiveImage().labelRects; for (let i = 0; i < labelRects.length; i++) { - const rect: IRect = this.calculateRectRelativeToActiveImage(labelRects[i].rect); - const rectAnchor = this.getAnchorUnderMouseByRect(rect); - if (!!rectAnchor) return labelRects[i]; + if (this.isMouseOverRectEdges(labelRects[i].rect)) { + return labelRects[i]; + } } return null; } + private isMouseOverRectEdges(rect: IRect): boolean { + const rectOnImage: IRect = RectUtil.translate( + this.calculateRectRelativeToActiveImage(rect), this.imageRectOnCanvas); + + const outerRectDelta: IPoint = { + x: this.config.anchorHoverSize.width / 2, + y: this.config.anchorHoverSize.height / 2 + }; + const outerRect: IRect = RectUtil.expand(rectOnImage, outerRectDelta); + + const innerRectDelta: IPoint = { + x: - this.config.anchorHoverSize.width / 2, + y: - this.config.anchorHoverSize.height / 2 + }; + const innerRect: IRect = RectUtil.expand(rectOnImage, innerRectDelta); + + return (RectUtil.isPointInside(outerRect, this.mousePosition) && + !RectUtil.isPointInside(innerRect, this.mousePosition)); + } + private getAnchorUnderMouseByRect(rect: IRect): RectAnchor { const rectAnchors: RectAnchor[] = RectUtil.mapRectToAnchors(rect); for (let i = 0; i < rectAnchors.length; i++) { @@ -314,4 +353,14 @@ export class RectRenderEngine extends BaseRenderEngine { y: scaledRect.y + this.imageRectOnCanvas.y } } + + private snapPointToImage(point: IPoint): IPoint { + if (RectUtil.isPointInside(this.imageRectOnCanvas, point)) + return point; + + return { + x: NumberUtil.snapValueToRange(point.x, this.imageRectOnCanvas.x, this.imageRectOnCanvas.x + this.imageRectOnCanvas.width), + y: NumberUtil.snapValueToRange(point.y, this.imageRectOnCanvas.y, this.imageRectOnCanvas.y + this.imageRectOnCanvas.height) + } + } } \ No newline at end of file diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 975072e3..a67caa6c 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -19,7 +19,7 @@ export class Settings { public static readonly DARK_THEME_SECOND_COLOR: string = "#282828"; public static readonly DARK_THEME_THIRD_COLOR: string = "#4c4c4c"; - public static readonly CANVAS_PADDING_WIDTH_PX: number = 10; + 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"; diff --git a/src/store/Actions.ts b/src/store/Actions.ts index 749ffec0..15f738d4 100644 --- a/src/store/Actions.ts +++ b/src/store/Actions.ts @@ -1,5 +1,6 @@ export enum Action { UPDATE_PROJECT_TYPE = '@@UPDATE_PROJECT_TYPE', + UPDATE_PROJECT_NAME = '@@UPDATE_PROJECT_NAME', UPDATE_ACTIVE_IMAGE_INDEX = '@@UPDATE_ACTIVE_IMAGE_INDEX', UPDATE_IMAGE_DATA_BY_ID = '@@UPDATE_IMAGE_DATA_BY_ID', ADD_IMAGES_DATA = '@@ADD_IMAGES_DATA', diff --git a/src/store/editor/actionCreators.ts b/src/store/editor/actionCreators.ts index 35ac88a8..6ea08d0d 100644 --- a/src/store/editor/actionCreators.ts +++ b/src/store/editor/actionCreators.ts @@ -12,6 +12,15 @@ export function updateProjectType(projectType: ProjectType): EditorActionTypes { }; } +export function updateProjectName(projectName: string): EditorActionTypes { + return { + type: Action.UPDATE_PROJECT_NAME, + payload: { + projectName, + }, + }; +} + export function updateActiveImageIndex(activeImageIndex: number): EditorActionTypes { return { type: Action.UPDATE_ACTIVE_IMAGE_INDEX, diff --git a/src/store/editor/reducer.ts b/src/store/editor/reducer.ts index c4dbbf19..804e0227 100644 --- a/src/store/editor/reducer.ts +++ b/src/store/editor/reducer.ts @@ -8,6 +8,7 @@ const initialState: EditorState = { activeLabelId: null, highlightedLabelId: null, projectType: null, + projectName: "my-project-name", imagesData: [], labelNames: [], firstLabelCreatedFlag: false @@ -24,6 +25,12 @@ export function editorReducer( projectType: action.payload.projectType } } + case Action.UPDATE_PROJECT_NAME: { + return { + ...state, + projectName: action.payload.projectName + } + } case Action.UPDATE_ACTIVE_IMAGE_INDEX: { return { ...state, diff --git a/src/store/editor/types.ts b/src/store/editor/types.ts index 780e5a89..1c971e0a 100644 --- a/src/store/editor/types.ts +++ b/src/store/editor/types.ts @@ -31,6 +31,7 @@ export type EditorState = { activeLabelId: string; highlightedLabelId: string; projectType: ProjectType; + projectName: string, imagesData: ImageData[]; labelNames: string[]; firstLabelCreatedFlag: boolean; @@ -43,6 +44,13 @@ interface UpdateProjectType { } } +interface UpdateProjectName { + type: typeof Action.UPDATE_PROJECT_NAME; + payload: { + projectName: string; + } +} + interface UpdateActiveImageIndex { type: typeof Action.UPDATE_ACTIVE_IMAGE_INDEX; payload: { @@ -115,6 +123,7 @@ interface UpdateFirstLabelCreatedFlag { } export type EditorActionTypes = UpdateProjectType + | UpdateProjectName | UpdateActiveImageIndex | UpdateActiveLabelNameIndex | UpdateActiveLabelType diff --git a/src/store/selectors/EditorSelector.ts b/src/store/selectors/EditorSelector.ts new file mode 100644 index 00000000..70332eca --- /dev/null +++ b/src/store/selectors/EditorSelector.ts @@ -0,0 +1,16 @@ +import {store} from "../.."; +import {ImageData} from "../editor/types"; + +export class EditorSelector { + public static getProjectName(): string { + return store.getState().editor.projectName; + } + + public static getLabelNames(): string[] { + return store.getState().editor.labelNames; + } + + public static getImagesData(): ImageData[] { + return store.getState().editor.imagesData; + } +} \ No newline at end of file diff --git a/src/utils/CanvasUtil.ts b/src/utils/CanvasUtil.ts index 5aa590b2..d1f06416 100644 --- a/src/utils/CanvasUtil.ts +++ b/src/utils/CanvasUtil.ts @@ -1,5 +1,7 @@ import React from "react"; import {IPoint} from "../interfaces/IPoint"; +import {IRect} from "../interfaces/IRect"; +import {ISize} from "../interfaces/ISize"; export class CanvasUtil { public static getMousePositionOnCanvasFromEvent(event: React.MouseEvent | MouseEvent, canvas: HTMLCanvasElement): IPoint { @@ -12,4 +14,28 @@ export class CanvasUtil { } return null; } + + public static getClientRect(canvas: HTMLCanvasElement): IRect { + if (!!canvas) { + const canvasRect: ClientRect | DOMRect = canvas.getBoundingClientRect(); + return { + x: canvasRect.left, + y: canvasRect.top, + width: canvasRect.width, + height: canvasRect.height + } + } + return null; + } + + public static getSize(canvas: HTMLCanvasElement): ISize { + if (!!canvas) { + const canvasRect: ClientRect | DOMRect = canvas.getBoundingClientRect(); + return { + width: canvasRect.width, + height: canvasRect.height + } + } + return null; + } } \ No newline at end of file diff --git a/src/utils/NumberUtil.ts b/src/utils/NumberUtil.ts new file mode 100644 index 00000000..e5ab948d --- /dev/null +++ b/src/utils/NumberUtil.ts @@ -0,0 +1,10 @@ +export class NumberUtil { + public static snapValueToRange(value: number, min: number, max: number): number { + if (value < min) + return min; + if (value > max) + return max; + + return value; + } +} \ No newline at end of file diff --git a/src/utils/RectUtil.ts b/src/utils/RectUtil.ts index dccb508c..8f0ddae1 100644 --- a/src/utils/RectUtil.ts +++ b/src/utils/RectUtil.ts @@ -117,6 +117,15 @@ export class RectUtil { } } + public static expand(rect: IRect, delta: IPoint): IRect { + return { + x: rect.x - delta.x, + y: rect.y - delta.y, + width: rect.width + 2 * delta.x, + height: rect.height + 2 * delta.y + } + } + public static mapRectToAnchors(rect: IRect): RectAnchor[] { return [ {type: AnchorType.TOP_LEFT, middlePosition: {x: rect.x, y: rect.y}}, diff --git a/src/utils/XMLSanitizerUtil.ts b/src/utils/XMLSanitizerUtil.ts new file mode 100644 index 00000000..e05b7aea --- /dev/null +++ b/src/utils/XMLSanitizerUtil.ts @@ -0,0 +1,10 @@ +export class XMLSanitizerUtil { + public static sanitize(input: string): string { + return input + .replace('<', '<') + .replace('>', '>') + .replace('&', '&') + .replace("'", ''') + .replace("/", '/') + } +} \ No newline at end of file diff --git a/src/views/Common/ImageButton/ImageButton.scss b/src/views/Common/ImageButton/ImageButton.scss index 0c21d381..8f21ac21 100644 --- a/src/views/Common/ImageButton/ImageButton.scss +++ b/src/views/Common/ImageButton/ImageButton.scss @@ -10,6 +10,10 @@ transition: background-color 0.7s ease; margin: 5px 2px; + > img { + user-select: none; + } + &:hover ~ .Cursor { width: 20px; height: 20px; diff --git a/src/views/Common/TextInput/TextInput.tsx b/src/views/Common/TextInput/TextInput.tsx index 32bdb73a..db3fa70f 100644 --- a/src/views/Common/TextInput/TextInput.tsx +++ b/src/views/Common/TextInput/TextInput.tsx @@ -5,7 +5,8 @@ interface IProps { key: string; label?: string; isPassword: boolean; - onChange: (value: string) => any; + onChange?: (event: React.ChangeEvent) => any; + onFocus?: (event: React.FocusEvent) => any; inputStyle?: React.CSSProperties; labelStyle?: React.CSSProperties; barStyle?: React.CSSProperties; @@ -19,9 +20,11 @@ const TextInput = (props: IProps) => { label, isPassword, onChange, + onFocus, inputStyle, labelStyle, - barStyle + barStyle, + value } = props; const getInputType = () => { @@ -31,11 +34,12 @@ const TextInput = (props: IProps) => { return (

onChange(event.target.value)} + onChange={onChange ? onChange : undefined} + onFocus={onFocus ? onFocus : undefined} /> {!!label &&