@abidibo/react-cam-roi 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +36 -10
  2. package/dist/Components/Button/Button.module.css +4 -2
  3. package/dist/Components/EnumField/index.d.ts +4 -4
  4. package/dist/Components/RoiEditor/Canvas.js +3 -3
  5. package/dist/Components/RoiEditor/Header.d.ts +2 -0
  6. package/dist/Components/RoiEditor/Header.js +22 -0
  7. package/dist/Components/RoiEditor/Header.module.css +32 -0
  8. package/dist/Components/RoiEditor/Hooks.js +25 -24
  9. package/dist/Components/RoiEditor/ParametersModalForm/index.d.ts +8 -2
  10. package/dist/Components/RoiEditor/ParametersModalForm/index.js +28 -19
  11. package/dist/Components/RoiEditor/Polygon.d.ts +4 -4
  12. package/dist/Components/RoiEditor/Polygon.js +5 -4
  13. package/dist/Components/RoiEditor/Polyline.d.ts +4 -4
  14. package/dist/Components/RoiEditor/Polyline.js +5 -4
  15. package/dist/Components/RoiEditor/Rectangle.d.ts +3 -3
  16. package/dist/Components/RoiEditor/Rectangle.js +5 -4
  17. package/dist/Components/RoiEditor/RoisInfo.d.ts +2 -0
  18. package/dist/Components/RoiEditor/RoisInfo.js +43 -0
  19. package/dist/Components/RoiEditor/ShapesList.js +51 -21
  20. package/dist/Components/RoiEditor/ShapesList.module.css +32 -1
  21. package/dist/Components/RoiEditor/Toolbar.js +5 -13
  22. package/dist/Components/RoiEditor/Toolbar.module.css +0 -12
  23. package/dist/Components/RoiEditor/Types.d.ts +4 -0
  24. package/dist/Components/RoiEditor/Utils.js +27 -20
  25. package/dist/Components/RoiEditor/index.d.ts +1 -1
  26. package/dist/Components/RoiEditor/index.js +7 -3
  27. package/dist/Components/RoleField.d.ts +7 -0
  28. package/dist/Components/RoleField.js +35 -0
  29. package/dist/Providers/EditorProvider.d.ts +2 -2
  30. package/dist/Providers/EditorProvider.js +2 -2
  31. package/dist/Providers/UiProvider.d.ts +12 -1
  32. package/dist/Providers/UiProvider.js +13 -1
  33. package/dist/Utils/index.d.ts +2 -0
  34. package/dist/Utils/index.js +12 -0
  35. package/package.json +2 -2
@@ -0,0 +1,43 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useContext } from 'react';
3
+ import { useEditorContext } from '../../Providers/EditorProvider';
4
+ import { UiContext } from '../../Providers/UiProvider';
5
+ import { OperatorEnum } from './Types';
6
+ import { formatString, humanize } from '../../Utils';
7
+ const RoisInfo = () => {
8
+ var _a;
9
+ const { strings, Typography } = useContext(UiContext);
10
+ const { configuration } = useEditorContext();
11
+ if (!((_a = configuration.rois) === null || _a === void 0 ? void 0 : _a.length))
12
+ return null;
13
+ return (_jsxs("div", { children: [_jsxs(Typography, { component: "div", children: [strings.roisToBeDrawn, ":"] }), _jsx("ul", { children: configuration.rois.map(r => {
14
+ var _a, _b;
15
+ let rule;
16
+ const data = {
17
+ role: humanize(r.role),
18
+ type: r.type,
19
+ threshold: (_a = r.multiplicity) === null || _a === void 0 ? void 0 : _a.threshold,
20
+ };
21
+ switch ((_b = r.multiplicity) === null || _b === void 0 ? void 0 : _b.operator) {
22
+ case OperatorEnum.Eq:
23
+ rule = formatString(strings.roiMultiplicityEqRule, data);
24
+ break;
25
+ case OperatorEnum.Lt:
26
+ rule = formatString(strings.roiMultiplicityLtRule, data);
27
+ break;
28
+ case OperatorEnum.Lte:
29
+ rule = formatString(strings.roiMultiplicityLteRule, data);
30
+ break;
31
+ case OperatorEnum.Gt:
32
+ rule = formatString(strings.roiMultiplicityGtRule, data);
33
+ break;
34
+ case OperatorEnum.Gte:
35
+ rule = formatString(strings.roiMultiplicityGteRule, data);
36
+ break;
37
+ default:
38
+ rule = formatString(strings.roiMultiplicityNoRule, data);
39
+ }
40
+ return (_jsx("li", { children: rule }, r.role));
41
+ }) })] }));
42
+ };
43
+ export default RoisInfo;
@@ -2,46 +2,76 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useContext, useEffect, useState } from 'react';
3
3
  import { useEditorContext } from '../../Providers/EditorProvider';
4
4
  import { UiContext } from '../../Providers/UiProvider';
5
- import { css } from '../../Utils';
5
+ import { css, humanize } from '../../Utils';
6
6
  import Dispatcher from '../../Utils/Dispatcher';
7
7
  import ParametersModalForm from './ParametersModalForm';
8
8
  import styles from './ShapesList.module.css';
9
9
  const ShapesList = () => {
10
- var _a, _b, _c, _d, _e, _f;
10
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
11
11
  const { strings, Typography, IconButton, DeleteIcon, AnnotateIcon, SelectIcon, CopyIcon, themeMode } = useContext(UiContext);
12
- const { shapes, removeShape, configuration, metadata, setMetadata } = useEditorContext();
12
+ const { shapes, removeShape, configuration, metadata, setMetadata, addShape, editorId } = useEditorContext();
13
13
  const [selected, setSelected] = useState([]);
14
- const [form, setForm] = useState({ isOpen: false, shapeId: '' });
14
+ const [form, setForm] = useState({
15
+ isOpen: false,
16
+ shapeId: '',
17
+ type: null,
18
+ shape: null,
19
+ });
20
+ // open metadata form immediately after drawing the shape
21
+ useEffect(() => {
22
+ const openForm = (_, { id, type, shape }) => {
23
+ setForm({ isOpen: true, shapeId: id, type, shape });
24
+ };
25
+ Dispatcher.register(`canvas:${editorId}:shapeAdded`, openForm);
26
+ return () => {
27
+ Dispatcher.unregister(`canvas:${editorId}:shapeAdded`, openForm);
28
+ };
29
+ }, [editorId]);
15
30
  useEffect(() => {
16
31
  const setSelectedShapes = (_, event) => { var _a; return setSelected((_a = event === null || event === void 0 ? void 0 : event.map((s) => s.id)) !== null && _a !== void 0 ? _a : []); };
17
- Dispatcher.register('canvas:shapeSelected', setSelectedShapes);
32
+ Dispatcher.register(`canvas:${editorId}:shapeSelected`, setSelectedShapes);
18
33
  return () => {
19
- Dispatcher.unregister('canvas:shapeSelected', setSelectedShapes);
34
+ Dispatcher.unregister(`canvas:${editorId}:shapeSelected`, setSelectedShapes);
20
35
  };
21
- }, [shapes]);
22
- const handleCopyShape = (id) => () => {
23
- Dispatcher.emit('canvas:copyShape', id);
36
+ }, [shapes, editorId]);
37
+ const handleCopyShape = (id) => (evt) => {
38
+ evt.stopPropagation();
39
+ Dispatcher.emit(`canvas:${editorId}:copyShape`, id);
24
40
  };
25
41
  const handleRemoveShape = (id) => () => {
26
- Dispatcher.emit('canvas:removeShape', id);
42
+ Dispatcher.emit(`canvas:${editorId}:removeShape`, id);
27
43
  removeShape(id);
28
44
  };
29
45
  const handleSelectShape = (id) => () => {
30
- Dispatcher.emit('canvas:selectShape', id);
46
+ Dispatcher.emit(`canvas:${editorId}:selectShape`, id);
31
47
  };
32
48
  const handleEditShapeMetadata = (id) => () => {
33
- setForm({ isOpen: true, shapeId: id });
49
+ setForm({ isOpen: true, shapeId: id, type: null, shape: null });
50
+ };
51
+ const handleSubmitMetadata = (shapeId) => (data, properties) => {
52
+ // if in creation mode, add the shape
53
+ if (form.type !== null) {
54
+ addShape(shapeId, form.type, form.shape);
55
+ }
56
+ setMetadata(Object.assign(Object.assign({}, metadata), { rois: [...metadata.rois.filter((r) => r.id !== shapeId), Object.assign({ id: shapeId, parameters: data }, properties)] }));
57
+ setForm({ isOpen: false, shapeId: '', type: null, shape: null });
34
58
  };
35
- const handleSubmitMetadata = (shapeId) => (data) => {
36
- setMetadata(Object.assign(Object.assign({}, metadata), { rois: [
37
- ...metadata.rois.filter((r) => r.id !== shapeId),
38
- { id: shapeId, parameters: data },
39
- ] }));
40
- setForm({ isOpen: false, shapeId: '' });
59
+ const handleCloseMetadataForm = () => {
60
+ // if in creation mode do not add shape and delete shape from canvas
61
+ if (form.type !== null) {
62
+ Dispatcher.emit(`canvas:${editorId}:removeShape`, form.shapeId);
63
+ }
64
+ setForm({ isOpen: false, shapeId: '', type: null, shape: null });
41
65
  };
42
66
  const iconColor = themeMode === 'light' ? 'black' : 'white';
43
- return (_jsxs(_Fragment, { children: [_jsxs("table", { className: css('shapes-table', styles, themeMode), children: [Object.keys(shapes).length > 0 && (_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: _jsx(Typography, { style: { fontWeight: 'bold' }, children: strings.id }) }), _jsx("th", { children: _jsx(Typography, { style: { fontWeight: 'bold' }, children: strings.type }) }), _jsx("th", {})] }) })), _jsx("tbody", { children: Object.keys(shapes).map((id) => {
44
- return (_jsxs("tr", { className: selected.indexOf(id) > -1 ? css('shapes-row-selected', styles, themeMode) : '', children: [_jsx("td", { children: _jsx("div", { children: _jsx(Typography, { children: id.substring(0, 6) }) }) }), _jsx("td", { children: _jsx(Typography, { children: strings[shapes[id].type] }) }), _jsxs("td", { children: [_jsx(IconButton, { onClick: handleSelectShape(id), children: _jsx(SelectIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleCopyShape(id), children: _jsx(CopyIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleEditShapeMetadata(id), disabled: !configuration.rois.find((roi) => roi.type === shapes[id].type), children: _jsx(AnnotateIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleRemoveShape(id), children: _jsx(DeleteIcon, { color: iconColor }) })] })] }, id));
45
- }) })] }), form.isOpen && (_jsx(ParametersModalForm, { parameters: (_b = (_a = configuration.rois.find((roi) => roi.type === shapes[form.shapeId].type)) === null || _a === void 0 ? void 0 : _a.parameters) !== null && _b !== void 0 ? _b : [], data: (_f = (_d = (_c = metadata.rois.find((roi) => roi.id === form.shapeId)) === null || _c === void 0 ? void 0 : _c.parameters) !== null && _d !== void 0 ? _d : (_e = configuration.rois.find((roi) => roi.type === shapes[form.shapeId].type)) === null || _e === void 0 ? void 0 : _e.parameters) !== null && _f !== void 0 ? _f : [], title: strings.mainParametersMetadata, onClose: () => setForm({ isOpen: false, shapeId: '' }), onSubmit: handleSubmitMetadata(form.shapeId) }))] }));
67
+ return (_jsxs(_Fragment, { children: [_jsxs("table", { className: css('shapes-table', styles, themeMode), children: [Object.keys(shapes).length > 0 && (_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: _jsx(Typography, { style: { fontWeight: 'bold' }, children: strings.name }) }), _jsx("th", { children: _jsx(Typography, { style: { fontWeight: 'bold' }, children: strings.role }) }), _jsx("th", { children: _jsx(Typography, { style: { fontWeight: 'bold' }, children: strings.type }) }), _jsx("th", {})] }) })), _jsx("tbody", { children: Object.keys(shapes).map((id, idx) => {
68
+ var _a;
69
+ const m = metadata.rois.find((roi) => roi.id === id);
70
+ return (_jsxs("tr", { onClick: handleSelectShape(id), className: selected.indexOf(id) > -1
71
+ ? css('shapes-row-selected', styles, themeMode)
72
+ : idx % 2 === 0
73
+ ? css('shapes-row-even', styles, themeMode)
74
+ : css('shapes-row-odd', styles, themeMode), children: [_jsx("td", { children: _jsxs("div", { className: styles.shapesTableName, children: [_jsx("div", { className: styles.shapesTableColor, style: { backgroundColor: shapes[id].shape.stroke } }), _jsx(Typography, { children: m === null || m === void 0 ? void 0 : m.name })] }) }), _jsx("td", { children: _jsx(Typography, { children: humanize((_a = m === null || m === void 0 ? void 0 : m.role) !== null && _a !== void 0 ? _a : '') }) }), _jsx("td", { children: _jsx(Typography, { children: strings[shapes[id].type] }) }), _jsxs("td", { children: [_jsx(IconButton, { onClick: handleSelectShape(id), children: _jsx(SelectIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleCopyShape(id), children: _jsx(CopyIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleEditShapeMetadata(id), children: _jsx(AnnotateIcon, { color: iconColor }) }), _jsx(IconButton, { onClick: handleRemoveShape(id), children: _jsx(DeleteIcon, { color: iconColor }) })] })] }, id));
75
+ }) })] }), form.isOpen && (_jsx(ParametersModalForm, { shapeType: form.type || shapes[form.shapeId].type, shapeName: (_b = (_a = metadata.rois.find((roi) => roi.id === form.shapeId)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '', shapeRole: (_d = (_c = metadata.rois.find((roi) => roi.id === form.shapeId)) === null || _c === void 0 ? void 0 : _c.role) !== null && _d !== void 0 ? _d : '', parameters: (_f = (_e = configuration.rois.find((roi) => roi.type === (form.type || shapes[form.shapeId].type))) === null || _e === void 0 ? void 0 : _e.parameters) !== null && _f !== void 0 ? _f : [], data: (_k = (_h = (_g = metadata.rois.find((roi) => roi.id === form.shapeId)) === null || _g === void 0 ? void 0 : _g.parameters) !== null && _h !== void 0 ? _h : (_j = configuration.rois.find((roi) => roi.type === (form.type || shapes[form.shapeId].type))) === null || _j === void 0 ? void 0 : _j.parameters) !== null && _k !== void 0 ? _k : [], title: strings.shapeParametersMetadata, onClose: handleCloseMetadataForm, onSubmit: handleSubmitMetadata(form.shapeId) }))] }));
46
76
  };
47
77
  export default ShapesList;
@@ -4,7 +4,7 @@
4
4
  }
5
5
 
6
6
  .shapes-table th {
7
- padding: .8rem .3rem .3rem;
7
+ padding: .8rem .3rem;
8
8
  text-align: left;
9
9
  }
10
10
 
@@ -34,3 +34,34 @@
34
34
  .shapes-row-selected-dark {
35
35
  background-color: #666;
36
36
  }
37
+
38
+ .shapes-row-even-light {
39
+ background-color: #f7f7f7;
40
+ }
41
+
42
+ .shapes-row-even-dark {
43
+ background-color: #444;
44
+ }
45
+
46
+ .shapes-row-odd-light {
47
+ background-color: #f0f0f0;
48
+ }
49
+
50
+ .shapes-row-odd-dark {
51
+ background-color: #555;
52
+ }
53
+
54
+ .shapesTableName {
55
+ display: flex;
56
+ align-items: center;
57
+ }
58
+
59
+ .shapesTableColor {
60
+ border: 1px solid #000;
61
+ box-sizing: border-box;
62
+ display: inline-block;
63
+ width: 24px;
64
+ height: 24px;
65
+ border-radius: 50%;
66
+ margin-right: .5rem;
67
+ }
@@ -1,33 +1,25 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useContext, useState } from 'react';
3
- import AnnotateIcon from '../../Icons/AnnotateIcon';
2
+ import { useContext } from 'react';
4
3
  import PointerIcon from '../../Icons/PointerIcon';
5
4
  import PolygonIcon from '../../Icons/PolygonIcon';
6
5
  import PolylineIcon from '../../Icons/PolylineIcon';
7
6
  import RectangleIcon from '../../Icons/RectangleIcon';
8
- import SaveIcon from '../../Icons/SaveIcon';
9
7
  import { useEditorContext } from '../../Providers/EditorProvider';
10
8
  import { UiContext } from '../../Providers/UiProvider';
11
9
  import { css } from '../../Utils';
12
10
  import ColorPicker from './ColorPicker';
13
- import ParametersModalForm from './ParametersModalForm';
14
11
  import styles from './Toolbar.module.css';
15
- import { enableMainMetadata, enableRois } from './Utils';
12
+ import { enableRois } from './Utils';
16
13
  const Toolbar = () => {
17
- var _a, _b;
14
+ var _a;
18
15
  const { IconButton, themeMode, primaryColor, Typography, strings } = useContext(UiContext);
19
- const { activeTool, setActiveTool, configuration, metadata, setMetadata, onSubmit } = useEditorContext();
20
- const [form, setForm] = useState({ isOpen: false });
16
+ const { activeTool, setActiveTool, configuration } = useEditorContext();
21
17
  const iconColor = (tool) => (tool === activeTool ? primaryColor : themeMode === 'light' ? 'black' : 'white');
22
18
  const setTool = (tool) => () => setActiveTool(tool);
23
- const handleSubmitMetadata = (data) => {
24
- setMetadata(Object.assign(Object.assign({}, metadata), { parameters: data }));
25
- setForm({ isOpen: false });
26
- };
27
19
  const hideForbiddenTools = (_a = configuration.options) === null || _a === void 0 ? void 0 : _a.hideForbiddenTools;
28
20
  const polylineEnabled = configuration.rois.find((r) => r.type === "polyline" /* ToolEnum.Polyline */);
29
21
  const polygonEnabled = configuration.rois.find((r) => r.type === "polygon" /* ToolEnum.Polygon */);
30
22
  const rectangleEnabled = configuration.rois.find((r) => r.type === "rect" /* ToolEnum.Rectangle */);
31
- return (_jsxs(_Fragment, { children: [((_b = configuration.options) === null || _b === void 0 ? void 0 : _b.description) && (_jsx("div", { className: css('toolbar-info', styles, themeMode), children: _jsx(Typography, { children: configuration.options.description }) })), _jsxs("div", { className: css('toolbar', styles, themeMode), children: [enableRois(configuration) && (_jsxs(_Fragment, { children: [_jsx(IconButton, { onClick: setTool("pointer" /* ToolEnum.Pointer */), children: _jsx(PointerIcon, { color: iconColor("pointer" /* ToolEnum.Pointer */) }) }), (!hideForbiddenTools || polylineEnabled) && (_jsx(IconButton, { onClick: setTool("polyline" /* ToolEnum.Polyline */), disabled: !polylineEnabled, children: _jsx(PolylineIcon, { color: iconColor("polyline" /* ToolEnum.Polyline */) }) })), (!hideForbiddenTools || polygonEnabled) && (_jsx(IconButton, { onClick: setTool("polygon" /* ToolEnum.Polygon */), disabled: !polygonEnabled, children: _jsx(PolygonIcon, { color: iconColor("polygon" /* ToolEnum.Polygon */) }) })), (!hideForbiddenTools || rectangleEnabled) && (_jsx(IconButton, { onClick: setTool("rect" /* ToolEnum.Rectangle */), disabled: !rectangleEnabled, children: _jsx(RectangleIcon, { color: iconColor("rect" /* ToolEnum.Rectangle */) }) })), _jsx(ColorPicker, { style: { marginLeft: 'auto', marginRight: '.5rem' } })] })), enableRois(configuration) && enableMainMetadata(configuration) && (_jsx("div", { className: css('toolbar-spacer', styles, themeMode) })), enableMainMetadata(configuration) && (_jsx(IconButton, { onClick: () => setForm({ isOpen: true }), children: _jsx(AnnotateIcon, { color: iconColor("rect" /* ToolEnum.Rectangle */) }) })), _jsx("div", { className: css('toolbar-spacer', styles, themeMode) }), _jsx(IconButton, { onClick: onSubmit, children: _jsx(SaveIcon, { color: iconColor("rect" /* ToolEnum.Rectangle */) }) })] }), enableRois(configuration) && (_jsx("div", { className: css('toolbar-helper', styles, themeMode), children: _jsxs(Typography, { children: [strings[activeTool], ": ", strings[`${activeTool}HelpText`]] }) })), form.isOpen && (_jsx(ParametersModalForm, { parameters: configuration.parameters, data: metadata.parameters, title: strings.mainParametersMetadata, onClose: () => setForm({ isOpen: false }), onSubmit: handleSubmitMetadata }))] }));
23
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: css('toolbar', styles, themeMode), children: enableRois(configuration) && (_jsxs(_Fragment, { children: [_jsx(IconButton, { onClick: setTool("pointer" /* ToolEnum.Pointer */), children: _jsx(PointerIcon, { color: iconColor("pointer" /* ToolEnum.Pointer */) }) }), (!hideForbiddenTools || polylineEnabled) && (_jsx(IconButton, { onClick: setTool("polyline" /* ToolEnum.Polyline */), disabled: !polylineEnabled, children: _jsx(PolylineIcon, { color: iconColor("polyline" /* ToolEnum.Polyline */) }) })), (!hideForbiddenTools || polygonEnabled) && (_jsx(IconButton, { onClick: setTool("polygon" /* ToolEnum.Polygon */), disabled: !polygonEnabled, children: _jsx(PolygonIcon, { color: iconColor("polygon" /* ToolEnum.Polygon */) }) })), (!hideForbiddenTools || rectangleEnabled) && (_jsx(IconButton, { onClick: setTool("rect" /* ToolEnum.Rectangle */), disabled: !rectangleEnabled, children: _jsx(RectangleIcon, { color: iconColor("rect" /* ToolEnum.Rectangle */) }) })), _jsx(ColorPicker, { style: { marginLeft: 'auto', marginRight: '.5rem' } })] })) }), enableRois(configuration) && (_jsx("div", { className: css('toolbar-helper', styles, themeMode), children: _jsxs(Typography, { children: [strings[activeTool], ": ", strings[`${activeTool}HelpText`]] }) }))] }));
32
24
  };
33
25
  export default Toolbar;
@@ -1,15 +1,3 @@
1
- .toolbar-info {
2
- padding: .5rem;
3
- }
4
- .toolbar-info-light {
5
- background-color: #ddd;;
6
- color: #000;
7
- }
8
- .toolbar-info-dark {
9
- background-color: #222;
10
- color: #fff;
11
- }
12
-
13
1
  .toolbar {
14
2
  align-items: center;
15
3
  display: flex;
@@ -73,6 +73,8 @@ export type Metadata = {
73
73
  rois: {
74
74
  id: string;
75
75
  parameters: OutputParameter[];
76
+ name: string;
77
+ role: string;
76
78
  }[];
77
79
  };
78
80
  export type OutputShapeRect = {
@@ -107,6 +109,8 @@ export interface OutputParameter {
107
109
  export interface OutputRoi {
108
110
  parameters: OutputParameter[];
109
111
  type: ShapeType;
112
+ name: string;
113
+ role: string;
110
114
  shape: OutputShapeRect | OutputShapePolyline | OutputShapePolygon;
111
115
  }
112
116
  export interface Output {
@@ -1,4 +1,5 @@
1
- import { OperatorEnum } from './Types';
1
+ import { formatString } from '../../Utils';
2
+ import { OperatorEnum, } from './Types';
2
3
  export const notify = {
3
4
  info: (message) => alert(`Info: ${message}`),
4
5
  warn: (message) => alert(`Warning: ${message}`),
@@ -72,49 +73,55 @@ export const validate = (configuration, shapes, metadata, strings) => {
72
73
  switch (roi.multiplicity.operator) {
73
74
  case OperatorEnum.Eq:
74
75
  if (Object.values(shapes).filter((s) => s.type === roi.type).length !== roi.multiplicity.threshold) {
75
- errors.push(strings.shapesOfTypeShouldBeEqualToThreshold
76
- .replace('{type}', String(roi.type))
77
- .replace('{threshold}', roi.multiplicity.threshold.toString()));
76
+ errors.push(formatString(strings.shapesOfTypeShouldBeEqualToThreshold, {
77
+ type: String(roi.type),
78
+ threshold: roi.multiplicity.threshold,
79
+ }));
78
80
  }
79
81
  break;
80
82
  case OperatorEnum.Lt:
81
83
  if (Object.values(shapes).filter((s) => s.type === roi.type).length >= roi.multiplicity.threshold) {
82
- errors.push(strings.shapesOfTypeShouldBeLessThanThreshold
83
- .replace('{type}', String(roi.type))
84
- .replace('{threshold}', roi.multiplicity.threshold.toString()));
84
+ errors.push(formatString(strings.shapesOfTypeShouldBeLessThanThreshold, {
85
+ type: String(roi.type),
86
+ threshold: roi.multiplicity.threshold,
87
+ }));
85
88
  }
86
89
  break;
87
90
  case OperatorEnum.Lte:
88
91
  if (Object.values(shapes).filter((s) => s.type === roi.type).length > roi.multiplicity.threshold) {
89
- errors.push(strings.shapesOfTypeShouldBeLessThanOrEqualToThreshold
90
- .replace('{type}', String(roi.type))
91
- .replace('{threshold}', roi.multiplicity.threshold.toString()));
92
+ errors.push(formatString(strings.shapesOfTypeShouldBeLessThanOrEqualToThreshold, {
93
+ type: String(roi.type),
94
+ threshold: roi.multiplicity.threshold,
95
+ }));
92
96
  }
93
97
  break;
94
98
  case OperatorEnum.Gt:
95
99
  if (Object.values(shapes).filter((s) => s.type === roi.type).length <= roi.multiplicity.threshold) {
96
- errors.push(strings.shapesOfTypeShouldBeGreaterThanThreshold
97
- .replace('{type}', String(roi.type))
98
- .replace('{threshold}', roi.multiplicity.threshold.toString()));
100
+ errors.push(formatString(strings.shapesOfTypeShouldBeGreaterThanThreshold, {
101
+ type: String(roi.type),
102
+ threshold: roi.multiplicity.threshold,
103
+ }));
99
104
  }
100
105
  break;
101
106
  case OperatorEnum.Gte:
102
107
  if (Object.values(shapes).filter((s) => s.type === roi.type).length < roi.multiplicity.threshold) {
103
- errors.push(strings.shapesOfTypeShouldBeGreaterThanOrEqualToThreshold
104
- .replace('{type}', String(roi.type))
105
- .replace('{threshold}', roi.multiplicity.threshold.toString()));
108
+ errors.push(formatString(strings.shapesOfTypeShouldBeGreaterThanOrEqualToThreshold, {
109
+ type: String(roi.type),
110
+ threshold: roi.multiplicity.threshold,
111
+ }));
106
112
  }
107
113
  }
108
114
  }
109
115
  });
110
116
  // check rois metadata
111
- Object.keys(shapes).forEach(shapeId => {
117
+ Object.keys(shapes).forEach((shapeId) => {
112
118
  var _a, _b;
113
119
  const type = shapes[shapeId].type;
114
120
  const confParameters = (_b = (_a = configuration.rois.find((r) => r.type === type)) === null || _a === void 0 ? void 0 : _a.parameters) !== null && _b !== void 0 ? _b : [];
115
121
  confParameters.forEach((p) => {
116
122
  var _a, _b;
117
- if (p.required && isEmpty((_b = (_a = metadata.rois.find((r) => r.id === shapeId)) === null || _a === void 0 ? void 0 : _a.parameters.find((p) => p.codename === p.codename)) === null || _b === void 0 ? void 0 : _b.value)) {
123
+ if (p.required &&
124
+ isEmpty((_b = (_a = metadata.rois.find((r) => r.id === shapeId)) === null || _a === void 0 ? void 0 : _a.parameters.find((p) => p.codename === p.codename)) === null || _b === void 0 ? void 0 : _b.value)) {
118
125
  errors.push(strings.missingRequiredValuesInShapeParameters.replace('{id}', shapeId));
119
126
  }
120
127
  });
@@ -129,7 +136,7 @@ export const fabricShapeToOutputShape = (shape, type) => {
129
136
  left: shape.left,
130
137
  width: shape.width,
131
138
  height: shape.height,
132
- color: shape.stroke
139
+ color: shape.stroke,
133
140
  };
134
141
  case "polygon" /* ToolEnum.Polygon */:
135
142
  case "polyline" /* ToolEnum.Polyline */:
@@ -137,7 +144,7 @@ export const fabricShapeToOutputShape = (shape, type) => {
137
144
  points: shape.get('points'),
138
145
  top: shape.top,
139
146
  left: shape.left,
140
- color: shape.stroke
147
+ color: shape.stroke,
141
148
  };
142
149
  }
143
150
  };
@@ -4,7 +4,7 @@ export type RoiEditorProps = {
4
4
  configuration: Configuration;
5
5
  onSubmit: (data: Output) => void;
6
6
  initialData?: Output;
7
- id: string;
7
+ editorId: string;
8
8
  };
9
9
  declare const RoiEditor: React.FC<RoiEditorProps>;
10
10
  export default RoiEditor;
@@ -10,8 +10,9 @@ import styles from './RoiEditor.module.css';
10
10
  import ShapesList from './ShapesList';
11
11
  import Toolbar from './Toolbar';
12
12
  import { fabricShapeToOutputShape, validate } from './Utils';
13
+ import Header from './Header';
13
14
  // https://github.com/n-mazaheri/image-editor
14
- const RoiEditor = ({ imageUrl, configuration, onSubmit, initialData, id }) => {
15
+ const RoiEditor = ({ imageUrl, configuration, onSubmit, initialData, editorId }) => {
15
16
  var _a, _b, _c;
16
17
  const { themeMode, enableLogs, pickerColors, strings, notify } = useContext(UiContext);
17
18
  const { imageSize, canvasSize, wrapperRef, isReady } = useCanvasSize(imageUrl);
@@ -38,6 +39,7 @@ const RoiEditor = ({ imageUrl, configuration, onSubmit, initialData, id }) => {
38
39
  setShapes(newShapes);
39
40
  setMetadata(Object.assign(Object.assign({}, metadata), { rois: metadata.rois.filter((r) => r.id !== id) }));
40
41
  }, [shapes, metadata]);
42
+ // TODO: go with percentage coordinates or pixel coordinates but relative to original image
41
43
  const handleSubmit = useCallback(() => {
42
44
  var _a, _b;
43
45
  const [isValid, errors] = validate(configuration, shapes, metadata, strings);
@@ -45,10 +47,12 @@ const RoiEditor = ({ imageUrl, configuration, onSubmit, initialData, id }) => {
45
47
  onSubmit({
46
48
  parameters: (_b = (_a = metadata.parameters) === null || _a === void 0 ? void 0 : _a.map((p) => ({ codename: p.codename, value: p.value }))) !== null && _b !== void 0 ? _b : [],
47
49
  rois: Object.keys(shapes).map((shapeId) => {
48
- var _a, _b, _c;
50
+ var _a, _b, _c, _d, _e, _f, _g;
49
51
  return ({
50
52
  parameters: (_c = (_b = (_a = metadata.rois
51
53
  .find((r) => r.id === shapeId)) === null || _a === void 0 ? void 0 : _a.parameters) === null || _b === void 0 ? void 0 : _b.map((p) => ({ codename: p.codename, value: p.value }))) !== null && _c !== void 0 ? _c : [],
54
+ name: (_e = (_d = metadata.rois.find((r) => r.id === shapeId)) === null || _d === void 0 ? void 0 : _d.name) !== null && _e !== void 0 ? _e : '',
55
+ role: (_g = (_f = metadata.rois.find((r) => r.id === shapeId)) === null || _f === void 0 ? void 0 : _f.role) !== null && _g !== void 0 ? _g : '',
52
56
  type: shapes[shapeId].type,
53
57
  shape: fabricShapeToOutputShape(shapes[shapeId].shape, shapes[shapeId].shape.type),
54
58
  });
@@ -65,7 +69,7 @@ const RoiEditor = ({ imageUrl, configuration, onSubmit, initialData, id }) => {
65
69
  if (!isReady) {
66
70
  return _jsx(Loader, {});
67
71
  }
68
- return (_jsx(EditorProvider, { id: id, hideForbiddenTools: (_c = (_b = configuration.options) === null || _b === void 0 ? void 0 : _b.hideForbiddenTools) !== null && _c !== void 0 ? _c : false, activeTool: activeTool, setActiveTool: setActiveTool, activeColor: activeColor, setActiveColor: setActiveColor, shapes: shapes, addShape: addShape, addShapes: addShapes, removeShape: removeShape, configuration: configuration, metadata: metadata, setMetadata: setMetadata, onSubmit: handleSubmit, children: _jsxs("div", { style: { maxWidth: '100%', width: `${imageSize.width}px` }, ref: wrapperRef, children: [_jsx(Toolbar, {}), _jsx("div", { className: css('canvasWrapper', styles, themeMode), style: {
72
+ return (_jsx(EditorProvider, { editorId: editorId, hideForbiddenTools: (_c = (_b = configuration.options) === null || _b === void 0 ? void 0 : _b.hideForbiddenTools) !== null && _c !== void 0 ? _c : false, activeTool: activeTool, setActiveTool: setActiveTool, activeColor: activeColor, setActiveColor: setActiveColor, shapes: shapes, addShape: addShape, addShapes: addShapes, removeShape: removeShape, configuration: configuration, metadata: metadata, setMetadata: setMetadata, onSubmit: handleSubmit, children: _jsxs("div", { style: { maxWidth: '100%', width: `${imageSize.width}px` }, ref: wrapperRef, children: [_jsx(Header, {}), _jsx(Toolbar, {}), _jsx("div", { className: css('canvasWrapper', styles, themeMode), style: {
69
73
  width: `${canvasSize.width}px`,
70
74
  height: `${canvasSize.height}px`,
71
75
  backgroundImage: `url(${imageUrl})`,
@@ -0,0 +1,7 @@
1
+ import { FieldProps } from '../Types';
2
+ import { ShapeType } from './RoiEditor/Types';
3
+ type RoleFieldProps = Omit<FieldProps<string>, 'readonly' | 'label'> & {
4
+ shapeType: ShapeType;
5
+ };
6
+ declare const RoleField: React.FC<RoleFieldProps>;
7
+ export default RoleField;
@@ -0,0 +1,35 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { useContext, useEffect } from 'react';
14
+ import { useEditorContext } from '../Providers/EditorProvider';
15
+ import { UiContext } from '../Providers/UiProvider';
16
+ import EnumField from './EnumField';
17
+ import { humanize } from '../Utils';
18
+ const RoleField = (_a) => {
19
+ var { onChange, value, required, shapeType } = _a, props = __rest(_a, ["onChange", "value", "required", "shapeType"]);
20
+ const { strings } = useContext(UiContext);
21
+ const { configuration } = useEditorContext();
22
+ const options = [];
23
+ (configuration.rois || []).filter(r => r.type === shapeType).forEach((r) => {
24
+ if (!options.includes(r.role)) {
25
+ options.push(r.role);
26
+ }
27
+ });
28
+ useEffect(() => {
29
+ if (required) {
30
+ onChange(options[0]);
31
+ }
32
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
33
+ return (_jsx(EnumField, Object.assign({ required: required, label: strings.role, value: value, onChange: (value) => onChange(value), options: options.map((o) => ({ value: o, label: humanize(o) })) }, props)));
34
+ };
35
+ export default RoleField;
@@ -18,9 +18,9 @@ type EditorContextType = {
18
18
  metadata: Metadata;
19
19
  setMetadata: (data: Metadata) => void;
20
20
  onSubmit: () => void;
21
- id: string;
21
+ editorId: string;
22
22
  };
23
23
  export declare const EditorContext: import("react").Context<EditorContextType | undefined>;
24
24
  export declare function useEditorContext(): EditorContextType;
25
- declare const EditorProvider: ({ children, id, hideForbiddenTools, activeTool, setActiveTool, activeColor, setActiveColor, shapes, addShape, addShapes, removeShape, configuration, metadata, setMetadata, onSubmit, }: PropsWithChildren<EditorContextType>) => import("react/jsx-runtime").JSX.Element;
25
+ declare const EditorProvider: ({ children, editorId, hideForbiddenTools, activeTool, setActiveTool, activeColor, setActiveColor, shapes, addShape, addShapes, removeShape, configuration, metadata, setMetadata, onSubmit, }: PropsWithChildren<EditorContextType>) => import("react/jsx-runtime").JSX.Element;
26
26
  export default EditorProvider;
@@ -8,9 +8,9 @@ export function useEditorContext() {
8
8
  }
9
9
  return context;
10
10
  }
11
- const EditorProvider = ({ children, id, hideForbiddenTools, activeTool, setActiveTool, activeColor, setActiveColor, shapes, addShape, addShapes, removeShape, configuration, metadata, setMetadata, onSubmit, }) => {
11
+ const EditorProvider = ({ children, editorId, hideForbiddenTools, activeTool, setActiveTool, activeColor, setActiveColor, shapes, addShape, addShapes, removeShape, configuration, metadata, setMetadata, onSubmit, }) => {
12
12
  return (_jsx(EditorContext.Provider, { value: {
13
- id,
13
+ editorId,
14
14
  hideForbiddenTools,
15
15
  activeTool,
16
16
  setActiveTool,
@@ -20,6 +20,7 @@ type UiContextType = {
20
20
  enableLogs: boolean;
21
21
  themeMode: 'light' | 'dark';
22
22
  primaryColor: string;
23
+ primaryFgColor: string;
23
24
  Typography: typeof Typography;
24
25
  IconButton: typeof IconButton;
25
26
  Modal: typeof Modal;
@@ -47,6 +48,13 @@ type UiContextType = {
47
48
  mainParametersMetadata: string;
48
49
  missingRequiredValuesInMainParameters: string;
49
50
  missingRequiredValuesInShapeParameters: string;
51
+ roiMultiplicityEqRule: string;
52
+ roiMultiplicityGtRule: string;
53
+ roiMultiplicityGteRule: string;
54
+ roiMultiplicityLtRule: string;
55
+ roiMultiplicityLteRule: string;
56
+ roiMultiplicityNoRule: string;
57
+ name: string;
50
58
  polygon: string;
51
59
  polygonHelpText: string;
52
60
  polyline: string;
@@ -55,8 +63,11 @@ type UiContextType = {
55
63
  rectHelpText: string;
56
64
  pointer: string;
57
65
  pointerHelpText: string;
66
+ roisToBeDrawn: string;
67
+ role: string;
58
68
  requiredField: string;
59
69
  save: string;
70
+ shapeParametersMetadata: string;
60
71
  shapesOfTypeShouldBeEqualToThreshold: string;
61
72
  shapesOfTypeShouldBeGreaterThanThreshold: string;
62
73
  shapesOfTypeShouldBeGreaterThanOrEqualToThreshold: string;
@@ -67,7 +78,7 @@ type UiContextType = {
67
78
  };
68
79
  export declare const DefaultUiContext: UiContextType;
69
80
  export declare const UiContext: import("react").Context<UiContextType>;
70
- declare const UiProvider: ({ children, enableLogs, themeMode, primaryColor, Typography, Modal, IconButton, DeleteIcon, EditIcon, SelectIcon, CopyIcon, AnnotateIcon, SaveIcon, CloseIcon, TextField, NumberField, BoolField, EnumField, Button, pickerColors, notify, strings, }: PropsWithChildren<Partial<Omit<UiContextType, "strings">> & {
81
+ declare const UiProvider: ({ children, enableLogs, themeMode, primaryColor, primaryFgColor, Typography, Modal, IconButton, DeleteIcon, EditIcon, SelectIcon, CopyIcon, AnnotateIcon, SaveIcon, CloseIcon, TextField, NumberField, BoolField, EnumField, Button, pickerColors, notify, strings, }: PropsWithChildren<Partial<Omit<UiContextType, "strings">> & {
71
82
  strings?: Partial<UiContextType["strings"]>;
72
83
  }>) => import("react/jsx-runtime").JSX.Element;
73
84
  export default UiProvider;
@@ -20,6 +20,7 @@ export const DefaultUiContext = {
20
20
  enableLogs: true,
21
21
  themeMode: 'light',
22
22
  primaryColor: '#1976d2',
23
+ primaryFgColor: '#fff',
23
24
  Typography,
24
25
  IconButton,
25
26
  Modal,
@@ -47,6 +48,7 @@ export const DefaultUiContext = {
47
48
  mainParametersMetadata: 'Main parameters',
48
49
  missingRequiredValuesInMainParameters: 'Missing required values in main parameters',
49
50
  missingRequiredValuesInShapeParameters: 'Missing required values in shape {id} parameters',
51
+ name: 'Name',
50
52
  polygon: 'Polygon',
51
53
  polygonHelpText: 'click to draw all the polygon points, double click on the last point to close the polygon',
52
54
  polyline: 'Polyline',
@@ -56,7 +58,16 @@ export const DefaultUiContext = {
56
58
  pointer: 'Selection',
57
59
  pointerHelpText: 'click a shape to select it',
58
60
  requiredField: 'This field is required',
61
+ roiMultiplicityEqRule: 'a number of {role} ({type}) equal to {threshold}',
62
+ roiMultiplicityGtRule: 'a number of {role} ({type}) greater than {threshold}',
63
+ roiMultiplicityGteRule: 'a number of {role} ({type}) greater than or equal to {threshold}',
64
+ roiMultiplicityLtRule: 'a number of {role} ({type}) less than {threshold}',
65
+ roiMultiplicityLteRule: 'a number of {role} ({type}) less than or equal to {threshold}',
66
+ roiMultiplicityNoRule: 'a number of {role} ({type})',
67
+ roisToBeDrawn: 'ROIs to be drawn',
68
+ role: 'Role',
59
69
  save: 'Save',
70
+ shapeParametersMetadata: 'Shape parameters',
60
71
  shapesOfTypeShouldBeEqualToThreshold: 'Shapes of type {type} should be equal to {threshold}',
61
72
  shapesOfTypeShouldBeGreaterThanThreshold: 'Shapes of type {type} should be greater than {threshold}',
62
73
  shapesOfTypeShouldBeGreaterThanOrEqualToThreshold: 'Shapes of type {type} should be greater than or equal to {threshold}',
@@ -66,7 +77,7 @@ export const DefaultUiContext = {
66
77
  },
67
78
  };
68
79
  export const UiContext = createContext(DefaultUiContext);
69
- const UiProvider = ({ children, enableLogs, themeMode, primaryColor, Typography, Modal, IconButton, DeleteIcon, EditIcon, SelectIcon, CopyIcon, AnnotateIcon, SaveIcon, CloseIcon, TextField, NumberField, BoolField, EnumField, Button, pickerColors, notify, strings, }) => {
80
+ const UiProvider = ({ children, enableLogs, themeMode, primaryColor, primaryFgColor, Typography, Modal, IconButton, DeleteIcon, EditIcon, SelectIcon, CopyIcon, AnnotateIcon, SaveIcon, CloseIcon, TextField, NumberField, BoolField, EnumField, Button, pickerColors, notify, strings, }) => {
70
81
  const ctx = {
71
82
  enableLogs: enableLogs !== null && enableLogs !== void 0 ? enableLogs : DefaultUiContext.enableLogs,
72
83
  Typography: Typography !== null && Typography !== void 0 ? Typography : DefaultUiContext.Typography,
@@ -86,6 +97,7 @@ const UiProvider = ({ children, enableLogs, themeMode, primaryColor, Typography,
86
97
  Button: Button !== null && Button !== void 0 ? Button : DefaultUiContext.Button,
87
98
  themeMode: themeMode !== null && themeMode !== void 0 ? themeMode : DefaultUiContext.themeMode,
88
99
  primaryColor: primaryColor !== null && primaryColor !== void 0 ? primaryColor : DefaultUiContext.primaryColor,
100
+ primaryFgColor: primaryFgColor !== null && primaryFgColor !== void 0 ? primaryFgColor : DefaultUiContext.primaryFgColor,
89
101
  pickerColors: pickerColors !== null && pickerColors !== void 0 ? pickerColors : DefaultUiContext.pickerColors,
90
102
  notify: notify !== null && notify !== void 0 ? notify : DefaultUiContext.notify,
91
103
  strings: strings ? Object.assign(Object.assign({}, DefaultUiContext.strings), strings) : DefaultUiContext.strings,
@@ -1,2 +1,4 @@
1
1
  export declare const log: (level: "log" | "info" | "warn" | "error", enable: boolean, ...args: unknown[]) => false | void;
2
2
  export declare const css: (name: string, styles: Record<string, string>, themeMode: "light" | "dark" | null) => string;
3
+ export declare const humanize: (str: string) => string;
4
+ export declare const formatString: (str: string, placeholders: Record<string, string | number>) => string;
@@ -1,2 +1,14 @@
1
1
  export const log = (level, enable, ...args) => enable && console[level](...args);
2
2
  export const css = (name, styles, themeMode) => `${styles[name]} ${styles[`${name}-${themeMode}`]} react-cam-roi-${name}${themeMode ? `react-cam-roi-${name}-${themeMode}` : ''}`;
3
+ export const humanize = (str) => {
4
+ return str
5
+ .replace(/([A-Z])/g, ' $1')
6
+ .replace('_', ' ')
7
+ .replace(/^./, (str) => str.toUpperCase());
8
+ };
9
+ export const formatString = (str, placeholders) => {
10
+ Object.keys(placeholders).forEach((key) => {
11
+ str = str.replace(`{${key}}`, placeholders[key].toString());
12
+ });
13
+ return str;
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abidibo/react-cam-roi",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A react component for drawing ROI over images and managing metadata",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "copy-files": "copyfiles -u 1 src/**/*.module.css dist/",
32
32
  "test": "echo \"Error: no test specified\" && exit 1",
33
33
  "storybook": "storybook dev -p 6006",
34
- "build-storybook": "storybook build"
34
+ "build-storybook": "storybook build -o docs"
35
35
  },
36
36
  "author": "abidibo",
37
37
  "license": "MIT",