@searpent/react-image-annotate 2.0.75 → 2.0.77

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 (224) hide show
  1. package/.babelrc +6 -0
  2. package/.env +1 -0
  3. package/.flowconfig +2 -0
  4. package/.github/workflows/release-on-master.yml +32 -0
  5. package/.github/workflows/test.yml +16 -0
  6. package/.prettierrc +3 -0
  7. package/.releaserc.js +18 -0
  8. package/.storybook/addons.js +2 -0
  9. package/.storybook/config.js +16 -0
  10. package/LICENSE +21 -0
  11. package/package.json +1 -1
  12. package/public/favicon.ico +0 -0
  13. package/public/index.html +38 -0
  14. package/src/Annotator/bike-pic.png +0 -0
  15. package/src/Annotator/bike-pic2.png +0 -0
  16. package/src/Annotator/dab-keyframes.story.json +1 -0
  17. package/src/Annotator/exampleImages.js +48 -0
  18. package/src/Annotator/examplePhotos.js +7603 -0
  19. package/src/Annotator/index.js +380 -0
  20. package/src/Annotator/index.story.js +899 -0
  21. package/src/Annotator/poses.story.js +150 -0
  22. package/src/Annotator/reducers/combine-reducers.js +7 -0
  23. package/src/Annotator/reducers/convert-expanding-line-to-polygon.js +53 -0
  24. package/{Annotator → src/Annotator}/reducers/fix-twisted.js +5 -3
  25. package/src/Annotator/reducers/general-reducer.js +1228 -0
  26. package/src/Annotator/reducers/get-active-image.js +21 -0
  27. package/src/Annotator/reducers/get-implied-video-regions.js +115 -0
  28. package/src/Annotator/reducers/history-handler.js +60 -0
  29. package/src/Annotator/reducers/image-reducer.js +23 -0
  30. package/src/Annotator/reducers/video-reducer.js +85 -0
  31. package/src/Annotator/video.story.js +51 -0
  32. package/src/ClassSelectionMenu/index.js +112 -0
  33. package/src/Crosshairs/index.js +64 -0
  34. package/src/DebugSidebarBox/index.js +36 -0
  35. package/src/DemoSite/Editor.js +235 -0
  36. package/src/DemoSite/ErrorBoundaryDialog.js +34 -0
  37. package/src/DemoSite/index.js +41 -0
  38. package/src/DemoSite/index.story.js +10 -0
  39. package/src/DemoSite/simple-segmentation-example.json +572 -0
  40. package/{Editor → src/Editor}/annotation-plugin/annotation.css +20 -0
  41. package/src/Editor/annotation-plugin/annotation.js +546 -0
  42. package/src/Editor/index.js +50 -0
  43. package/src/Editor/readOnly.js +31 -0
  44. package/{Editor → src/Editor}/tools.js +3 -2
  45. package/src/Errorer/index.js +13 -0
  46. package/src/FullImageSegmentationAnnotator/hard1.story.jpg +0 -0
  47. package/src/FullImageSegmentationAnnotator/hard2.story.jpg +0 -0
  48. package/src/FullImageSegmentationAnnotator/hard3.story.jpg +0 -0
  49. package/src/FullImageSegmentationAnnotator/index.js +7 -0
  50. package/src/FullImageSegmentationAnnotator/index.story.js +177 -0
  51. package/src/FullImageSegmentationAnnotator/orange.story.png +0 -0
  52. package/src/GroupSelectorSidebarBox/index.js +48 -0
  53. package/src/GroupsEditorSidebarBox/index.js +108 -0
  54. package/src/HelpSidebarBox/index.js +43 -0
  55. package/src/HighlightBox/index.js +143 -0
  56. package/src/HistorySidebarBox/index.js +78 -0
  57. package/src/ImageCanvas/dancing-man.story.jpg +0 -0
  58. package/src/ImageCanvas/index.js +515 -0
  59. package/src/ImageCanvas/index.story.js +314 -0
  60. package/src/ImageCanvas/mouse_mask.story.png +0 -0
  61. package/src/ImageCanvas/region-tools.js +171 -0
  62. package/src/ImageCanvas/seves_desk.story.jpg +0 -0
  63. package/{ImageCanvas → src/ImageCanvas}/styles.js +8 -12
  64. package/src/ImageCanvas/use-mouse.js +168 -0
  65. package/src/ImageCanvas/use-project-box.js +23 -0
  66. package/src/ImageCanvas/use-wasd-mode.js +50 -0
  67. package/src/ImageMask/index.js +127 -0
  68. package/src/ImageMask/load-image.js +32 -0
  69. package/src/ImageSelectorSidebarBox/index.js +54 -0
  70. package/src/KeyframeTimeline/get-time-string.js +25 -0
  71. package/src/KeyframeTimeline/index.js +223 -0
  72. package/src/KeyframesSelectorSidebarBox/index.js +93 -0
  73. package/src/LandingPage/content.md +57 -0
  74. package/src/LandingPage/github-markdown.css +964 -0
  75. package/src/LandingPage/index.js +147 -0
  76. package/src/Locker/index.js +13 -0
  77. package/src/MainLayout/RightSidebarItemsWrapper.js +21 -0
  78. package/src/MainLayout/icon-dictionary.js +79 -0
  79. package/src/MainLayout/index.js +564 -0
  80. package/src/MainLayout/index.story.js +240 -0
  81. package/{MainLayout → src/MainLayout}/styles.js +7 -6
  82. package/src/MainLayout/types.js +171 -0
  83. package/src/MainLayout/use-implied-video-regions.js +17 -0
  84. package/src/MetadataEditorSidebarBox/index.js +160 -0
  85. package/src/PageSelector/index.js +159 -0
  86. package/src/PointDistances/index.js +90 -0
  87. package/src/PreventScrollToParents/index.js +48 -0
  88. package/src/PreventScrollToParents/index.story.js +23 -0
  89. package/src/RegionLabel/index.js +236 -0
  90. package/{RegionLabel → src/RegionLabel}/styles.js +15 -12
  91. package/src/RegionSelectAndTransformBoxes/index.js +236 -0
  92. package/src/RegionSelectorSidebarBox/index.js +220 -0
  93. package/{RegionSelectorSidebarBox → src/RegionSelectorSidebarBox}/styles.js +14 -13
  94. package/src/RegionShapes/index.js +254 -0
  95. package/src/RegionTags/index.js +136 -0
  96. package/src/SettingsDialog/index.js +58 -0
  97. package/src/SettingsProvider/index.js +57 -0
  98. package/src/Shortcuts/ShortcutField.js +44 -0
  99. package/src/Shortcuts/index.js +129 -0
  100. package/src/ShortcutsManager/index.js +162 -0
  101. package/src/Sidebar/index.js +117 -0
  102. package/src/SidebarBoxContainer/index.js +93 -0
  103. package/src/SmallToolButton/index.js +57 -0
  104. package/src/TagsSidebarBox/index.js +93 -0
  105. package/src/TaskDescriptionSidebarBox/index.js +43 -0
  106. package/src/Theme/index.js +36 -0
  107. package/src/VideoOrImageCanvasBackground/index.js +170 -0
  108. package/src/colors.js +32 -0
  109. package/src/hooks/use-colors.js +95 -0
  110. package/src/hooks/use-event-callback.js +11 -0
  111. package/src/hooks/use-exclude-pattern.js +27 -0
  112. package/src/hooks/use-load-image.js +21 -0
  113. package/src/hooks/use-window-size.js +46 -0
  114. package/{hooks → src/hooks}/xpattern.js +1 -1
  115. package/src/hooks/xpattern.png +0 -0
  116. package/src/index.js +18 -0
  117. package/src/lib.js +7 -0
  118. package/src/screenshot.png +0 -0
  119. package/src/site.css +5 -0
  120. package/src/stories.js +2 -0
  121. package/src/utils/blocks-to-article.js +61 -0
  122. package/{utils → src/utils}/blocks-to-article.test.js +8 -5
  123. package/{utils → src/utils}/default-locked-until.js +1 -2
  124. package/{utils → src/utils}/filter-only-unique.js +1 -1
  125. package/src/utils/get-from-local-storage.js +7 -0
  126. package/src/utils/get-hotkey-help-text.js +11 -0
  127. package/src/utils/get-landmarks-with-transform.js +23 -0
  128. package/src/utils/photosToImages.js +67 -0
  129. package/src/utils/regions-groups.js +19 -0
  130. package/src/utils/regions-to-blocks.js +16 -0
  131. package/src/utils/saveable-actions-enum.js +5 -0
  132. package/src/utils/set-in-local-storage.js +6 -0
  133. package/src/utils/sleep.js +3 -0
  134. package/src/utils/uuid-to-hash.js +5 -0
  135. package/Annotator/exampleImages.js +0 -41
  136. package/Annotator/examplePhotos.js +0 -6980
  137. package/Annotator/index.js +0 -417
  138. package/Annotator/reducers/combine-reducers.js +0 -14
  139. package/Annotator/reducers/convert-expanding-line-to-polygon.js +0 -73
  140. package/Annotator/reducers/general-reducer.js +0 -1430
  141. package/Annotator/reducers/get-active-image.js +0 -27
  142. package/Annotator/reducers/get-implied-video-regions.js +0 -180
  143. package/Annotator/reducers/history-handler.js +0 -38
  144. package/Annotator/reducers/image-reducer.js +0 -20
  145. package/Annotator/reducers/video-reducer.js +0 -88
  146. package/ClassSelectionMenu/index.js +0 -140
  147. package/Crosshairs/index.js +0 -53
  148. package/DebugSidebarBox/index.js +0 -20
  149. package/DemoSite/Editor.js +0 -194
  150. package/DemoSite/ErrorBoundaryDialog.js +0 -64
  151. package/DemoSite/index.js +0 -40
  152. package/Editor/annotation-plugin/annotation.js +0 -647
  153. package/Editor/index.js +0 -93
  154. package/Editor/readOnly.js +0 -73
  155. package/Errorer/index.js +0 -11
  156. package/FullImageSegmentationAnnotator/index.js +0 -7
  157. package/GroupSelectorSidebarBox/index.js +0 -63
  158. package/GroupsEditorSidebarBox/index.js +0 -138
  159. package/HelpSidebarBox/index.js +0 -58
  160. package/HighlightBox/index.js +0 -102
  161. package/HistorySidebarBox/index.js +0 -71
  162. package/ImageCanvas/index.js +0 -441
  163. package/ImageCanvas/region-tools.js +0 -165
  164. package/ImageCanvas/use-mouse.js +0 -180
  165. package/ImageCanvas/use-project-box.js +0 -27
  166. package/ImageCanvas/use-wasd-mode.js +0 -62
  167. package/ImageMask/index.js +0 -133
  168. package/ImageMask/load-image.js +0 -25
  169. package/ImageSelectorSidebarBox/index.js +0 -60
  170. package/KeyframeTimeline/get-time-string.js +0 -27
  171. package/KeyframeTimeline/index.js +0 -233
  172. package/KeyframesSelectorSidebarBox/index.js +0 -93
  173. package/LandingPage/index.js +0 -159
  174. package/Locker/index.js +0 -11
  175. package/MainLayout/RightSidebarItemsWrapper.js +0 -19
  176. package/MainLayout/icon-dictionary.js +0 -104
  177. package/MainLayout/index.js +0 -526
  178. package/MainLayout/types.js +0 -0
  179. package/MainLayout/use-implied-video-regions.js +0 -13
  180. package/MetadataEditorSidebarBox/index.js +0 -231
  181. package/PageSelector/index.js +0 -180
  182. package/PointDistances/index.js +0 -73
  183. package/PreventScrollToParents/index.js +0 -51
  184. package/RegionLabel/index.js +0 -232
  185. package/RegionSelectAndTransformBoxes/index.js +0 -169
  186. package/RegionSelectorSidebarBox/index.js +0 -254
  187. package/RegionShapes/index.js +0 -294
  188. package/RegionTags/index.js +0 -144
  189. package/SettingsDialog/index.js +0 -52
  190. package/SettingsProvider/index.js +0 -60
  191. package/Shortcuts/ShortcutField.js +0 -46
  192. package/Shortcuts/index.js +0 -133
  193. package/ShortcutsManager/index.js +0 -155
  194. package/Sidebar/index.js +0 -69
  195. package/SidebarBoxContainer/index.js +0 -93
  196. package/SmallToolButton/index.js +0 -42
  197. package/TagsSidebarBox/index.js +0 -105
  198. package/TaskDescriptionSidebarBox/index.js +0 -58
  199. package/Theme/index.js +0 -30
  200. package/VideoOrImageCanvasBackground/index.js +0 -151
  201. package/colors.js +0 -14
  202. package/hooks/use-colors.js +0 -97
  203. package/hooks/use-event-callback.js +0 -10
  204. package/hooks/use-exclude-pattern.js +0 -24
  205. package/hooks/use-load-image.js +0 -26
  206. package/hooks/use-window-size.js +0 -46
  207. package/index.js +0 -3
  208. package/lib.js +0 -3
  209. package/stories.js +0 -5
  210. package/utils/blocks-to-article.js +0 -60
  211. package/utils/get-from-local-storage.js +0 -7
  212. package/utils/get-hotkey-help-text.js +0 -9
  213. package/utils/get-landmarks-with-transform.js +0 -40
  214. package/utils/photosToImages.js +0 -85
  215. package/utils/regions-groups.js +0 -28
  216. package/utils/regions-to-blocks.js +0 -18
  217. package/utils/saveable-actions-enum.js +0 -3
  218. package/utils/set-in-local-storage.js +0 -3
  219. package/utils/sleep.js +0 -7
  220. package/utils/uuid-to-hash.js +0 -5
  221. /package/{Errorer → src/Errorer}/errorer.css +0 -0
  222. /package/{Locker → src/Locker}/locker.css +0 -0
  223. /package/{PageSelector → src/PageSelector}/page-selector.css +0 -0
  224. /package/{utils → src/utils}/next-group-id.js +0 -0
@@ -0,0 +1,1228 @@
1
+ // @flow
2
+ import type { MainLayoutState, Action } from "../../MainLayout/types"
3
+ import { moveRegion } from "../../ImageCanvas/region-tools.js"
4
+ import { getIn, setIn, updateIn } from "seamless-immutable"
5
+ import moment from "moment"
6
+ import isEqual from "lodash/isEqual"
7
+ import uniq from "lodash/uniq"
8
+ import getActiveImage from "./get-active-image"
9
+ import { saveToHistory } from "./history-handler.js"
10
+ import colors from "../../colors"
11
+ import fixTwisted from "./fix-twisted"
12
+ import convertExpandingLineToPolygon from "./convert-expanding-line-to-polygon"
13
+ import clamp from "clamp"
14
+ import getLandmarksWithTransform from "../../utils/get-landmarks-with-transform"
15
+ import setInLocalStorage from "../../utils/set-in-local-storage"
16
+ import onlyUnique from "../../utils/filter-only-unique"
17
+ import nextGroupId from "../../utils/next-group-id"
18
+ import defaultLockedUntil from "../../utils/default-locked-until"
19
+
20
+ const getRandomId = () => Math.random().toString().split(".")[1]
21
+
22
+ export default (state: MainLayoutState, action: Action) => {
23
+ if (
24
+ state.allowedArea &&
25
+ state.selectedTool !== "modify-allowed-area" &&
26
+ ["MOUSE_DOWN", "MOUSE_UP", "MOUSE_MOVE"].includes(action.type)
27
+ ) {
28
+ const aa = state.allowedArea
29
+ action.x = clamp(action.x, aa.x, aa.x + aa.w)
30
+ action.y = clamp(action.y, aa.y, aa.y + aa.h)
31
+ }
32
+
33
+ if (action.type === "ON_CLS_ADDED" && action.cls && action.cls !== "") {
34
+ const oldRegionClsList = state.regionClsList
35
+ const newState = {
36
+ ...state,
37
+ regionClsList: oldRegionClsList.concat(action.cls),
38
+ }
39
+ return newState
40
+ }
41
+
42
+ // Throttle certain actions
43
+ if (action.type === "MOUSE_MOVE") {
44
+ if (Date.now() - ((state: any).lastMouseMoveCall || 0) < 16) return state
45
+ state = setIn(state, ["lastMouseMoveCall"], Date.now())
46
+ }
47
+ if (!action.type.includes("MOUSE")) {
48
+ state = setIn(state, ["lastAction"], action)
49
+ }
50
+
51
+ const { currentImageIndex, pathToActiveImage, activeImage } =
52
+ getActiveImage(state)
53
+
54
+ const getRegionIndex = (region) => {
55
+ const regionId =
56
+ typeof region === "string" || typeof region === "number"
57
+ ? region
58
+ : region.id
59
+ if (!activeImage) return null
60
+ const regionIndex = (activeImage.regions || []).findIndex(
61
+ (r) => r.id === regionId
62
+ )
63
+ return regionIndex === -1 ? null : regionIndex
64
+ }
65
+ const getRegion = (regionId) => {
66
+ if (!activeImage) return null
67
+ const regionIndex = getRegionIndex(regionId)
68
+ if (regionIndex === null) return [null, null]
69
+ const region = activeImage.regions[regionIndex]
70
+ return [region, regionIndex]
71
+ }
72
+ const modifyRegion = (regionId, obj) => {
73
+ const [region, regionIndex] = getRegion(regionId)
74
+ if (!region) return state
75
+ if (obj !== null) {
76
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
77
+ ...region,
78
+ ...obj,
79
+ })
80
+ } else {
81
+ // delete region
82
+ const regions = activeImage.regions
83
+ return setIn(
84
+ state,
85
+ [...pathToActiveImage, "regions"],
86
+ (regions || []).filter((r) => r.id !== region.id)
87
+ )
88
+ }
89
+ }
90
+ const unselectRegions = (state: MainLayoutState) => {
91
+ if (!activeImage) return state
92
+ return setIn(
93
+ state,
94
+ [...pathToActiveImage, "regions"],
95
+ (activeImage.regions || []).map((r) => ({
96
+ ...r,
97
+ highlighted: false,
98
+ }))
99
+ )
100
+ }
101
+
102
+ const closeEditors = (state: MainLayoutState) => {
103
+ if (currentImageIndex === null) return state
104
+ return setIn(
105
+ state,
106
+ [...pathToActiveImage, "regions"],
107
+ (activeImage.regions || []).map((r) => ({
108
+ ...r,
109
+ editingLabels: false,
110
+ }))
111
+ )
112
+ }
113
+
114
+ const addSaveableAction = (state: MainLayoutState, action: string) => {
115
+ if (currentImageIndex === null) return state
116
+ return setIn(
117
+ state,
118
+ [...pathToActiveImage, "saveableActions"],
119
+ [...(activeImage.saveableActions || []), action]
120
+ )
121
+ }
122
+
123
+ const setNewImage = (img: string | Object, index: number) => {
124
+ let { src, frameTime } = typeof img === "object" ? img : { src: img }
125
+ state = setIn(state, ["previouslySelectedImage"], state.selectedImage)
126
+ return setIn(
127
+ setIn(state, ["selectedImage"], index),
128
+ ["selectedImageFrameTime"],
129
+ frameTime
130
+ )
131
+ }
132
+
133
+ switch (action.type) {
134
+ case "@@INIT": {
135
+ return state
136
+ }
137
+ case "SELECT_IMAGE": {
138
+ return setNewImage(action.image, action.imageIndex)
139
+ }
140
+ case "SELECT_CLASSIFICATION": {
141
+ // if there is selected region, set its cls
142
+ const selectedRegionId = state.images[state.selectedImage].regions.find(r => r.highlighted === true)?.id
143
+ if (selectedRegionId) {
144
+ const newRegions = activeImage.regions.map(r => {
145
+ if (r.id === selectedRegionId) {
146
+ return {
147
+ ...r,
148
+ cls: action.cls
149
+ }
150
+ } else {
151
+ return r
152
+ }
153
+ })
154
+ state = setIn(state, [...pathToActiveImage, "regions"], newRegions)
155
+ }
156
+ state = addSaveableAction(state, "SELECT_CLASSIFICATION")
157
+ return setIn(state, ["selectedCls"], action.cls)
158
+ }
159
+ case "CHANGE_REGION": {
160
+ const regionIndex = getRegionIndex(action.region)
161
+ if (regionIndex === null) return state
162
+ const oldRegion = activeImage.regions[regionIndex]
163
+ if (oldRegion.cls !== action.region.cls) {
164
+ state = saveToHistory(state, "Change Region Classification")
165
+ const clsIndex = state.regionClsList.indexOf(action.region.cls)
166
+ if (clsIndex !== -1) {
167
+ state = setIn(state, ["selectedCls"], action.region.cls)
168
+ action.region.color = colors[clsIndex % colors.length]
169
+ }
170
+ }
171
+ if (!isEqual(oldRegion.tags, action.region.tags)) {
172
+ state = saveToHistory(state, "Change Region Tags")
173
+ }
174
+ if (!isEqual(oldRegion.comment, action.region.comment)) {
175
+ state = saveToHistory(state, "Change Region Comment")
176
+ }
177
+ state = addSaveableAction(state, "CHANGE_REGION")
178
+ return setIn(
179
+ state,
180
+ [...pathToActiveImage, "regions", regionIndex],
181
+ action.region
182
+ )
183
+ }
184
+ case "CHANGE_IMAGE": {
185
+ if (!activeImage) return state
186
+ const { delta } = action
187
+ for (const key of Object.keys(delta)) {
188
+ if (key === "cls") saveToHistory(state, "Change Image Class")
189
+ if (key === "tags") saveToHistory(state, "Change Image Tags")
190
+ state = setIn(state, [...pathToActiveImage, key], delta[key])
191
+ }
192
+ return state
193
+ }
194
+ case "SELECT_REGION": {
195
+ const { region } = action
196
+ const regionIndex = getRegionIndex(action.region)
197
+ if (regionIndex === null) return state
198
+ const regions = [...(activeImage.regions || [])].map((r) => ({
199
+ ...r,
200
+ highlighted: r.id === region.id,
201
+ groupHighlighted: (r.groupId && r.groupId === region.groupId) ? true : false,
202
+ editingLabels: region.ctrlKey ? r.id === region.id : false,
203
+ }))
204
+
205
+ const selectedGroupIds = regions.filter(i => i.highlighted).map(r => r.groupId || '').filter(onlyUnique);
206
+ if (selectedGroupIds.length === 1) {
207
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], selectedGroupIds[0])
208
+ }
209
+ if (selectedGroupIds.length === 0) {
210
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], null)
211
+ }
212
+ return setIn(state, [...pathToActiveImage, "regions"], regions)
213
+ }
214
+ case "BEGIN_MOVE_POINT": {
215
+ state = closeEditors(state)
216
+ return setIn(state, ["mode"], {
217
+ mode: "MOVE_REGION",
218
+ regionId: action.point.id,
219
+ })
220
+ }
221
+ case "BEGIN_BOX_TRANSFORM": {
222
+ const { box, directions } = action
223
+ state = closeEditors(state)
224
+ if (directions[0] === 0 && directions[1] === 0) {
225
+ return setIn(state, ["mode"], { mode: "MOVE_REGION", regionId: box.id })
226
+ } else {
227
+ return setIn(state, ["mode"], {
228
+ mode: "RESIZE_BOX",
229
+ regionId: box.id,
230
+ freedom: directions,
231
+ original: { x: box.x, y: box.y, w: box.w, h: box.h },
232
+ })
233
+ }
234
+ }
235
+ case "BEGIN_MOVE_POLYGON_POINT": {
236
+ const { polygon, pointIndex } = action
237
+ state = closeEditors(state)
238
+ if (
239
+ state.mode &&
240
+ state.mode.mode === "DRAW_POLYGON" &&
241
+ pointIndex === 0
242
+ ) {
243
+ return setIn(
244
+ modifyRegion(polygon, {
245
+ points: polygon.points.slice(0, -1),
246
+ open: false,
247
+ }),
248
+ ["mode"],
249
+ null
250
+ )
251
+ } else {
252
+ state = saveToHistory(state, "Move Polygon Point")
253
+ }
254
+ return setIn(state, ["mode"], {
255
+ mode: "MOVE_POLYGON_POINT",
256
+ regionId: polygon.id,
257
+ pointIndex,
258
+ })
259
+ }
260
+ case "BEGIN_MOVE_KEYPOINT": {
261
+ const { region, keypointId } = action
262
+ state = closeEditors(state)
263
+ state = saveToHistory(state, "Move Keypoint")
264
+ return setIn(state, ["mode"], {
265
+ mode: "MOVE_KEYPOINT",
266
+ regionId: region.id,
267
+ keypointId,
268
+ })
269
+ }
270
+ case "ADD_POLYGON_POINT": {
271
+ const { polygon, point, pointIndex } = action
272
+ const regionIndex = getRegionIndex(polygon)
273
+ if (regionIndex === null) return state
274
+ const points = [...polygon.points]
275
+ points.splice(pointIndex, 0, point)
276
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
277
+ ...polygon,
278
+ points,
279
+ })
280
+ }
281
+ case "MOUSE_MOVE": {
282
+ const { x, y } = action
283
+ if (!state.mode) return state
284
+ if (!activeImage) return state
285
+ const { mouseDownAt } = state
286
+ switch (state.mode.mode) {
287
+ case "MOVE_POLYGON_POINT": {
288
+ const { pointIndex, regionId } = state.mode
289
+ const regionIndex = getRegionIndex(regionId)
290
+ if (regionIndex === null) return state
291
+ return setIn(
292
+ state,
293
+ [
294
+ ...pathToActiveImage,
295
+ "regions",
296
+ regionIndex,
297
+ "points",
298
+ pointIndex,
299
+ ],
300
+ [x, y]
301
+ )
302
+ }
303
+ case "MOVE_KEYPOINT": {
304
+ const { keypointId, regionId } = state.mode
305
+ const [region, regionIndex] = getRegion(regionId)
306
+ if (regionIndex === null) return state
307
+ return setIn(
308
+ state,
309
+ [
310
+ ...pathToActiveImage,
311
+ "regions",
312
+ regionIndex,
313
+ "points",
314
+ keypointId,
315
+ ],
316
+ { ...(region: any).points[keypointId], x, y }
317
+ )
318
+ }
319
+ case "MOVE_REGION": {
320
+ const { regionId } = state.mode
321
+ if (regionId === "$$allowed_area") {
322
+ const {
323
+ allowedArea: { w, h },
324
+ } = state
325
+ return setIn(state, ["allowedArea"], {
326
+ x: x - w / 2,
327
+ y: y - h / 2,
328
+ w,
329
+ h,
330
+ })
331
+ }
332
+ const regionIndex = getRegionIndex(regionId)
333
+ if (regionIndex === null) return state
334
+ return setIn(
335
+ state,
336
+ [...pathToActiveImage, "regions", regionIndex],
337
+ moveRegion(activeImage.regions[regionIndex], x, y)
338
+ )
339
+ }
340
+ case "RESIZE_BOX": {
341
+ const {
342
+ regionId,
343
+ freedom: [xFree, yFree],
344
+ original: { x: ox, y: oy, w: ow, h: oh },
345
+ } = state.mode
346
+
347
+ const dx = xFree === 0 ? ox : xFree === -1 ? Math.min(ox + ow, x) : ox
348
+ const dw =
349
+ xFree === 0
350
+ ? ow
351
+ : xFree === -1
352
+ ? ow + (ox - dx)
353
+ : Math.max(0, ow + (x - ox - ow))
354
+ const dy = yFree === 0 ? oy : yFree === -1 ? Math.min(oy + oh, y) : oy
355
+ const dh =
356
+ yFree === 0
357
+ ? oh
358
+ : yFree === -1
359
+ ? oh + (oy - dy)
360
+ : Math.max(0, oh + (y - oy - oh))
361
+
362
+ // determine if we should switch the freedom
363
+ if (dw <= 0.001) {
364
+ state = setIn(state, ["mode", "freedom"], [xFree * -1, yFree])
365
+ }
366
+ if (dh <= 0.001) {
367
+ state = setIn(state, ["mode", "freedom"], [xFree, yFree * -1])
368
+ }
369
+
370
+ if (regionId === "$$allowed_area") {
371
+ return setIn(state, ["allowedArea"], {
372
+ x: dx,
373
+ w: dw,
374
+ y: dy,
375
+ h: dh,
376
+ })
377
+ }
378
+
379
+ const regionIndex = getRegionIndex(regionId)
380
+ if (regionIndex === null) return state
381
+ const box = activeImage.regions[regionIndex]
382
+
383
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
384
+ ...box,
385
+ x: dx,
386
+ w: dw,
387
+ y: dy,
388
+ h: dh,
389
+ })
390
+ }
391
+ case "RESIZE_KEYPOINTS": {
392
+ const { regionId, landmarks, centerX, centerY } = state.mode
393
+ const distFromCenter = Math.sqrt(
394
+ (centerX - x) ** 2 + (centerY - y) ** 2
395
+ )
396
+ const scale = distFromCenter / 0.15
397
+ return modifyRegion(regionId, {
398
+ points: getLandmarksWithTransform({
399
+ landmarks,
400
+ center: { x: centerX, y: centerY },
401
+ scale,
402
+ }),
403
+ })
404
+ }
405
+ case "DRAW_POLYGON": {
406
+ const { regionId } = state.mode
407
+ const [region, regionIndex] = getRegion(regionId)
408
+ if (!region) return setIn(state, ["mode"], null)
409
+ return setIn(
410
+ state,
411
+ [
412
+ ...pathToActiveImage,
413
+ "regions",
414
+ regionIndex,
415
+ "points",
416
+ (region: any).points.length - 1,
417
+ ],
418
+ [x, y]
419
+ )
420
+ }
421
+ case "DRAW_LINE": {
422
+ const { regionId } = state.mode
423
+ const [region, regionIndex] = getRegion(regionId)
424
+ if (!region) return setIn(state, ["mode"], null)
425
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
426
+ ...region,
427
+ x2: x,
428
+ y2: y,
429
+ })
430
+ }
431
+ case "DRAW_EXPANDING_LINE": {
432
+ const { regionId } = state.mode
433
+ const [expandingLine, regionIndex] = getRegion(regionId)
434
+ if (!expandingLine) return state
435
+ const isMouseDown = Boolean(state.mouseDownAt)
436
+ if (isMouseDown) {
437
+ // If the mouse is down, set width/angle
438
+ const lastPoint = expandingLine.points.slice(-1)[0]
439
+ const mouseDistFromLastPoint = Math.sqrt(
440
+ (lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2
441
+ )
442
+ if (mouseDistFromLastPoint < 0.002 && !lastPoint.width) return state
443
+
444
+ const newState = setIn(
445
+ state,
446
+ [...pathToActiveImage, "regions", regionIndex, "points"],
447
+ expandingLine.points.slice(0, -1).concat([
448
+ {
449
+ ...lastPoint,
450
+ width: mouseDistFromLastPoint * 2,
451
+ angle: Math.atan2(lastPoint.x - x, lastPoint.y - y),
452
+ },
453
+ ])
454
+ )
455
+ return newState
456
+ } else {
457
+ // If mouse is up, move the next candidate point
458
+ return setIn(
459
+ state,
460
+ [...pathToActiveImage, "regions", regionIndex],
461
+ {
462
+ ...expandingLine,
463
+ candidatePoint: { x, y },
464
+ }
465
+ )
466
+ }
467
+
468
+ return state
469
+ }
470
+ case "SET_EXPANDING_LINE_WIDTH": {
471
+ const { regionId } = state.mode
472
+ const [expandingLine, regionIndex] = getRegion(regionId)
473
+ if (!expandingLine) return state
474
+ const lastPoint = expandingLine.points.slice(-1)[0]
475
+ const { mouseDownAt } = state
476
+ return setIn(
477
+ state,
478
+ [...pathToActiveImage, "regions", regionIndex, "expandingWidth"],
479
+ Math.sqrt((lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2)
480
+ )
481
+ }
482
+ default:
483
+ return state
484
+ }
485
+ }
486
+ case "MOUSE_DOWN": {
487
+ if (!activeImage) return state
488
+ const { x, y } = action
489
+
490
+ state = setIn(state, ["mouseDownAt"], { x, y })
491
+
492
+ if (state.mode) {
493
+ switch (state.mode.mode) {
494
+ case "DRAW_POLYGON": {
495
+ const [polygon, regionIndex] = getRegion(state.mode.regionId)
496
+ if (!polygon) break
497
+ return setIn(
498
+ state,
499
+ [...pathToActiveImage, "regions", regionIndex],
500
+ { ...polygon, points: polygon.points.concat([[x, y]]) }
501
+ )
502
+ }
503
+ case "DRAW_LINE": {
504
+ const [line, regionIndex] = getRegion(state.mode.regionId)
505
+ if (!line) break
506
+ setIn(state, [...pathToActiveImage, "regions", regionIndex], {
507
+ ...line,
508
+ x2: x,
509
+ y2: y,
510
+ })
511
+ return setIn(state, ["mode"], null)
512
+ }
513
+ case "DRAW_EXPANDING_LINE": {
514
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
515
+ if (!expandingLine) break
516
+ const lastPoint = expandingLine.points.slice(-1)[0]
517
+ if (
518
+ expandingLine.points.length > 1 &&
519
+ Math.sqrt((lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2) < 0.002
520
+ ) {
521
+ if (!lastPoint.width) {
522
+ return setIn(state, ["mode"], {
523
+ mode: "SET_EXPANDING_LINE_WIDTH",
524
+ regionId: state.mode.regionId,
525
+ })
526
+ } else {
527
+ return state
528
+ .setIn(
529
+ [...pathToActiveImage, "regions", regionIndex],
530
+ convertExpandingLineToPolygon(expandingLine)
531
+ )
532
+ .setIn(["mode"], null)
533
+ }
534
+ }
535
+
536
+ // Create new point
537
+ return setIn(
538
+ state,
539
+ [...pathToActiveImage, "regions", regionIndex, "points"],
540
+ expandingLine.points.concat([{ x, y, angle: null, width: null }])
541
+ )
542
+ }
543
+ case "SET_EXPANDING_LINE_WIDTH": {
544
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
545
+ if (!expandingLine) break
546
+ const { expandingWidth } = expandingLine
547
+ return state
548
+ .setIn(
549
+ [...pathToActiveImage, "regions", regionIndex],
550
+ convertExpandingLineToPolygon({
551
+ ...expandingLine,
552
+ points: expandingLine.points.map((p) =>
553
+ p.width ? p : { ...p, width: expandingWidth }
554
+ ),
555
+ expandingWidth: undefined,
556
+ })
557
+ )
558
+ .setIn(["mode"], null)
559
+ }
560
+ default:
561
+ break
562
+ }
563
+ }
564
+
565
+ let newRegion
566
+ let defaultRegionCls = state.selectedCls || state.regionClsList?.[0],
567
+ defaultRegionColor = "#ff0000"
568
+
569
+ const clsIndex = (state.regionClsList || []).indexOf(defaultRegionCls)
570
+ if (clsIndex !== -1) {
571
+ defaultRegionColor = colors[clsIndex % colors.length]
572
+ }
573
+
574
+ switch (state.selectedTool) {
575
+ case "create-point": {
576
+ state = saveToHistory(state, "Create Point")
577
+ newRegion = {
578
+ type: "point",
579
+ x,
580
+ y,
581
+ highlighted: true,
582
+ editingLabels: true,
583
+ color: defaultRegionColor,
584
+ id: getRandomId(),
585
+ cls: defaultRegionCls,
586
+ }
587
+ break
588
+ }
589
+ case "create-box": {
590
+ let groupId = state?.images[state.selectedImage]?.regions?.find(r => r.highlighted === true)?.groupId;
591
+ let groupSelected = true
592
+ if (groupId === undefined) {
593
+ groupId = nextGroupId()
594
+ groupSelected = false
595
+ }
596
+ state = saveToHistory(state, "Create Box")
597
+ newRegion = {
598
+ type: "box",
599
+ x: x,
600
+ y: y,
601
+ w: 0,
602
+ h: 0,
603
+ highlighted: true,
604
+ editingLabels: false,
605
+ color: defaultRegionColor,
606
+ cls: defaultRegionCls,
607
+ id: getRandomId(),
608
+ groupHighlighted: true,
609
+ groupId,
610
+ }
611
+ state = setIn(state, ["mode"], {
612
+ mode: "RESIZE_BOX",
613
+ editLabelEditorAfter: !groupSelected,
614
+ regionId: newRegion.id,
615
+ freedom: [1, 1],
616
+ original: { x, y, w: newRegion.w, h: newRegion.h },
617
+ isNew: true,
618
+ })
619
+ break
620
+ }
621
+ case "create-polygon": {
622
+ if (state.mode && state.mode.mode === "DRAW_POLYGON") break
623
+ state = saveToHistory(state, "Create Polygon")
624
+ newRegion = {
625
+ type: "polygon",
626
+ points: [
627
+ [x, y],
628
+ [x, y],
629
+ ],
630
+ open: true,
631
+ highlighted: true,
632
+ color: defaultRegionColor,
633
+ cls: defaultRegionCls,
634
+ id: getRandomId(),
635
+ }
636
+ state = setIn(state, ["mode"], {
637
+ mode: "DRAW_POLYGON",
638
+ regionId: newRegion.id,
639
+ })
640
+ break
641
+ }
642
+ case "create-expanding-line": {
643
+ state = saveToHistory(state, "Create Expanding Line")
644
+ newRegion = {
645
+ type: "expanding-line",
646
+ unfinished: true,
647
+ points: [{ x, y, angle: null, width: null }],
648
+ open: true,
649
+ highlighted: true,
650
+ color: defaultRegionColor,
651
+ cls: defaultRegionCls,
652
+ id: getRandomId(),
653
+ }
654
+ state = setIn(state, ["mode"], {
655
+ mode: "DRAW_EXPANDING_LINE",
656
+ regionId: newRegion.id,
657
+ })
658
+ break
659
+ }
660
+ case "create-line": {
661
+ if (state.mode && state.mode.mode === "DRAW_LINE") break
662
+ state = saveToHistory(state, "Create Line")
663
+ newRegion = {
664
+ type: "line",
665
+ x1: x,
666
+ y1: y,
667
+ x2: x,
668
+ y2: y,
669
+ highlighted: true,
670
+ editingLabels: false,
671
+ color: defaultRegionColor,
672
+ cls: defaultRegionCls,
673
+ id: getRandomId(),
674
+ }
675
+ state = setIn(state, ["mode"], {
676
+ mode: "DRAW_LINE",
677
+ regionId: newRegion.id,
678
+ })
679
+ break
680
+ }
681
+ case "create-keypoints": {
682
+ state = saveToHistory(state, "Create Keypoints")
683
+ const [[keypointsDefinitionId, { landmarks, connections }]] =
684
+ (Object.entries(state.keypointDefinitions): any)
685
+
686
+ newRegion = {
687
+ type: "keypoints",
688
+ keypointsDefinitionId,
689
+ points: getLandmarksWithTransform({
690
+ landmarks,
691
+ center: { x, y },
692
+ scale: 1,
693
+ }),
694
+ highlighted: true,
695
+ editingLabels: false,
696
+ id: getRandomId(),
697
+ }
698
+ state = setIn(state, ["mode"], {
699
+ mode: "RESIZE_KEYPOINTS",
700
+ landmarks,
701
+ centerX: x,
702
+ centerY: y,
703
+ regionId: newRegion.id,
704
+ isNew: true,
705
+ })
706
+ break
707
+ }
708
+ default:
709
+ break
710
+ }
711
+
712
+ const regions = [...(getIn(state, pathToActiveImage).regions || [])]
713
+ .map((r) =>
714
+ setIn(r, ["editingLabels"], false).setIn(["highlighted"], false).setIn([state.selectedTool === 'create-box' ? undefined : 'groupHighlighted'], false)
715
+ )
716
+ .concat(newRegion ? [newRegion] : [])
717
+
718
+ state = setIn(state, [...pathToActiveImage, "selectedGroupId"], null)
719
+
720
+ return setIn(state, [...pathToActiveImage, "regions"], regions)
721
+ }
722
+ case "MOUSE_UP": {
723
+ const { x, y } = action
724
+
725
+ const { mouseDownAt = { x, y } } = state
726
+ if (!state.mode) return state
727
+ state = setIn(state, ["mouseDownAt"], null)
728
+ switch (state.mode.mode) {
729
+ case "RESIZE_BOX": {
730
+ state = setIn(state, ["selectedTool"], 'select')
731
+ if (state.mode.isNew) {
732
+ if (
733
+ Math.abs(state.mode.original.x - x) < 0.002 ||
734
+ Math.abs(state.mode.original.y - y) < 0.002
735
+ ) {
736
+ state = addSaveableAction(state, "MOUSE_UP_RESIZE_BOX")
737
+ return setIn(
738
+ modifyRegion(state.mode.regionId, null),
739
+ ["mode"],
740
+ null
741
+ )
742
+ }
743
+ }
744
+ if (state.mode.editLabelEditorAfter) {
745
+ state = addSaveableAction(state, "MOUSE_UP_RESIZE_BOX")
746
+ return {
747
+ ...modifyRegion(state.mode.regionId, { editingLabels: true }),
748
+ mode: null,
749
+ }
750
+ }
751
+ }
752
+ case "MOVE_REGION":
753
+ case "RESIZE_KEYPOINTS":
754
+ case "MOVE_POLYGON_POINT": {
755
+ state = addSaveableAction(state, "MOUSE_UP_MOVE_REGION")
756
+ return { ...state, mode: null }
757
+ }
758
+ case "MOVE_KEYPOINT": {
759
+ return { ...state, mode: null }
760
+ }
761
+ case "CREATE_POINT_LINE": {
762
+ return state
763
+ }
764
+ case "DRAW_EXPANDING_LINE": {
765
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
766
+ if (!expandingLine) return state
767
+ let newExpandingLine = expandingLine
768
+ const lastPoint =
769
+ expandingLine.points.length !== 0
770
+ ? expandingLine.points.slice(-1)[0]
771
+ : mouseDownAt
772
+ let jointStart
773
+ if (expandingLine.points.length > 1) {
774
+ jointStart = expandingLine.points.slice(-2)[0]
775
+ } else {
776
+ jointStart = lastPoint
777
+ }
778
+ const mouseDistFromLastPoint = Math.sqrt(
779
+ (lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2
780
+ )
781
+ if (mouseDistFromLastPoint > 0.002) {
782
+ // The user is drawing has drawn the width for the last point
783
+ const newPoints = [...expandingLine.points]
784
+ for (let i = 0; i < newPoints.length - 1; i++) {
785
+ if (newPoints[i].width) continue
786
+ newPoints[i] = {
787
+ ...newPoints[i],
788
+ width: lastPoint.width,
789
+ }
790
+ }
791
+ newExpandingLine = setIn(
792
+ expandingLine,
793
+ ["points"],
794
+ fixTwisted(newPoints)
795
+ )
796
+ } else {
797
+ return state
798
+ }
799
+ return setIn(
800
+ state,
801
+ [...pathToActiveImage, "regions", regionIndex],
802
+ newExpandingLine
803
+ )
804
+ }
805
+ default:
806
+ return state
807
+ }
808
+ }
809
+ case "OPEN_REGION_EDITOR": {
810
+ const { region } = action
811
+ const regionIndex = getRegionIndex(action.region)
812
+ if (regionIndex === null) return state
813
+ const newRegions = setIn(
814
+ activeImage.regions.map((r) => ({
815
+ ...r,
816
+ highlighted: false,
817
+ editingLabels: false,
818
+ })),
819
+ [regionIndex],
820
+ {
821
+ ...(activeImage.regions || [])[regionIndex],
822
+ highlighted: true,
823
+ editingLabels: true,
824
+ }
825
+ )
826
+ return setIn(state, [...pathToActiveImage, "regions"], newRegions)
827
+ }
828
+ case "CLOSE_REGION_EDITOR": {
829
+ const { region } = action
830
+ const regionIndex = getRegionIndex(action.region)
831
+ if (regionIndex === null) return state
832
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
833
+ ...(activeImage.regions || [])[regionIndex],
834
+ editingLabels: false,
835
+ })
836
+ }
837
+ case "DELETE_REGION": {
838
+ const regionIndex = getRegionIndex(action.region)
839
+ if (regionIndex === null) return state
840
+ state = saveToHistory(state, "Delete region")
841
+ state = addSaveableAction(state, "DELETE_REGION")
842
+ return setIn(
843
+ state,
844
+ [...pathToActiveImage, "regions"],
845
+ (activeImage.regions || []).filter((r) => r.id !== action.region.id)
846
+ )
847
+ }
848
+ case "DELETE_GROUP": {
849
+ const { groupId } = action
850
+ if (groupId === null || groupId === undefined) return state
851
+ state = saveToHistory(state, "Delete group")
852
+ state = addSaveableAction(state, "DELETE_GROUP")
853
+ return setIn(
854
+ state,
855
+ [...pathToActiveImage, "regions"],
856
+ (activeImage.regions || []).filter((r) => r.groupId !== groupId)
857
+ )
858
+ }
859
+ case "DELETE_SELECTED_REGION": {
860
+ state = saveToHistory(state, "Delete selected region")
861
+ state = addSaveableAction(state, "DELETE_SELECTED_REGION")
862
+ return setIn(
863
+ state,
864
+ [...pathToActiveImage, "regions"],
865
+ (activeImage.regions || []).filter((r) => !r.highlighted)
866
+ )
867
+ }
868
+ case "HEADER_BUTTON_CLICKED": {
869
+ const buttonName = action.buttonName.toLowerCase()
870
+ switch (buttonName) {
871
+ case "prev": {
872
+ if (currentImageIndex === null) return state
873
+ if (currentImageIndex === 0) return state
874
+ return setNewImage(
875
+ state.images[currentImageIndex - 1],
876
+ currentImageIndex - 1
877
+ )
878
+ }
879
+ case "next": {
880
+ if (currentImageIndex === null) return state
881
+ if (currentImageIndex === state.images.length - 1) return state
882
+ return setNewImage(
883
+ state.images[currentImageIndex + 1],
884
+ currentImageIndex + 1
885
+ )
886
+ }
887
+ case "clone": {
888
+ if (currentImageIndex === null) return state
889
+ if (currentImageIndex === state.images.length - 1) return state
890
+ return setIn(
891
+ setNewImage(
892
+ state.images[currentImageIndex + 1],
893
+ currentImageIndex + 1
894
+ ),
895
+ ["images", currentImageIndex + 1, "regions"],
896
+ activeImage.regions
897
+ )
898
+ }
899
+ case "settings": {
900
+ return setIn(state, ["settingsOpen"], !state.settingsOpen)
901
+ }
902
+ case "help": {
903
+ return state
904
+ }
905
+ case "fullscreen": {
906
+ return setIn(state, ["fullScreen"], true)
907
+ }
908
+ case "exit fullscreen":
909
+ case "window": {
910
+ return setIn(state, ["fullScreen"], false)
911
+ }
912
+ case "hotkeys": {
913
+ return state
914
+ }
915
+ case "exit":
916
+ case "done": {
917
+ return state
918
+ }
919
+ default:
920
+ return state
921
+ }
922
+ }
923
+ case "SELECT_TOOL": {
924
+ if (action.selectedTool === "show-tags") {
925
+ setInLocalStorage("showTags", !state.showTags)
926
+ return setIn(state, ["showTags"], !state.showTags)
927
+ } else if (action.selectedTool === "show-mask") {
928
+ return setIn(state, ["showMask"], !state.showMask)
929
+ }
930
+ if (action.selectedTool === "modify-allowed-area" && !state.allowedArea) {
931
+ state = setIn(state, ["allowedArea"], { x: 0, y: 0, w: 1, h: 1 })
932
+ }
933
+ state = setIn(state, ["mode"], null)
934
+ return setIn(state, ["selectedTool"], action.selectedTool)
935
+ }
936
+ case "CANCEL": {
937
+ const { mode } = state
938
+ if (mode) {
939
+ switch (mode.mode) {
940
+ case "DRAW_EXPANDING_LINE":
941
+ case "SET_EXPANDING_LINE_WIDTH":
942
+ case "DRAW_POLYGON": {
943
+ const { regionId } = mode
944
+ return modifyRegion(regionId, null)
945
+ }
946
+ case "MOVE_POLYGON_POINT":
947
+ case "RESIZE_BOX":
948
+ case "MOVE_REGION": {
949
+ return setIn(state, ["mode"], null)
950
+ }
951
+ default:
952
+ return state
953
+ }
954
+ }
955
+ // Close any open boxes
956
+ const regions: any = activeImage.regions
957
+ if (regions && regions.some((r) => r.editingLabels)) {
958
+ return setIn(
959
+ state,
960
+ [...pathToActiveImage, "regions"],
961
+ regions.map((r) => ({
962
+ ...r,
963
+ editingLabels: false,
964
+ }))
965
+ )
966
+ } else if (regions) {
967
+ return setIn(
968
+ state,
969
+ [...pathToActiveImage, "regions"],
970
+ regions.map((r) => ({
971
+ ...r,
972
+ highlighted: false,
973
+ }))
974
+ )
975
+ }
976
+ break
977
+ }
978
+ case "UPDATE_REGIONS": {
979
+ const { imageIndex, regions: newRegions } = action;
980
+ const updatedRegions = state.images[imageIndex].regions.map(r => {
981
+ const updatedRegion = newRegions.find(i => i.id === r.id)
982
+ if (!updatedRegion) {
983
+ return r
984
+ }
985
+ return {
986
+ ...r,
987
+ cls: updatedRegion.cls,
988
+ text: updatedRegion.text
989
+ }
990
+ })
991
+ // TODO: add mutation of order and deletion of regions - SI-1967
992
+ state = addSaveableAction(state, "UPDATE_REGIONS")
993
+ return setIn(
994
+ state,
995
+ ["images", imageIndex, "regions"],
996
+ updatedRegions
997
+ )
998
+ }
999
+ case "IMAGES_UPDATED": {
1000
+ const { updatedAt } = action;
1001
+ return setIn(
1002
+ state,
1003
+ ["imagesUpdatedAt"],
1004
+ updatedAt
1005
+ )
1006
+ }
1007
+ case "IMAGES_SAVED": {
1008
+ const { savedAt } = action;
1009
+ return setIn(
1010
+ state,
1011
+ ["imagesSavedAt"],
1012
+ savedAt
1013
+ )
1014
+ }
1015
+ case "IMAGES_RECALCULATED": {
1016
+ const { recalculatedAt } = action;
1017
+ return setIn(
1018
+ state,
1019
+ ["imagesSavedAt"],
1020
+ recalculatedAt
1021
+ )
1022
+ }
1023
+ case "UPDATE_METADATA": {
1024
+ const { name, value, imageIndex, groupId, metadataId } = action;
1025
+ if (isNaN(imageIndex)) {
1026
+ // update global/album metadata
1027
+ const metadataIndex = state.albumMetadata?.findIndex(mt => mt.key === name)
1028
+ if (metadataIndex < 0) {
1029
+ console.error(`can't find metadata by key "${name}"`)
1030
+ return state;
1031
+ }
1032
+ state = addSaveableAction(state, "UPDATE_ALBUM_METADATA")
1033
+ return setIn(
1034
+ state,
1035
+ ["albumMetadata", metadataIndex],
1036
+ {
1037
+ key: name,
1038
+ value: value
1039
+ }
1040
+ )
1041
+ } else {
1042
+ // update metadata of article
1043
+ if (groupId) {
1044
+ const articleMetadataRegionIdx = state.images[imageIndex]?.regions.findIndex(r => r.cls === 'metadata' && r.groupId === groupId)
1045
+ if (articleMetadataRegionIdx < 0) {
1046
+ console.error(`can't find article metadata for goupId "${groupId}"`)
1047
+ return state
1048
+ }
1049
+
1050
+ const articleRegionToUpdate = state.images[imageIndex]?.regions[articleMetadataRegionIdx]
1051
+ const articleMetadata = JSON.parse(articleRegionToUpdate.text)
1052
+ const toBeUpdatedMetadataIdx = articleMetadata.findIndex(i => i.metadataId === metadataId)
1053
+ if (toBeUpdatedMetadataIdx < 0) {
1054
+ console.error(`can't find metadata field in article metadata for key "${name}"`)
1055
+ return state
1056
+ }
1057
+
1058
+ articleMetadata[toBeUpdatedMetadataIdx].value = value
1059
+ state = addSaveableAction(state, "UPDATE_METADATA")
1060
+ return setIn(
1061
+ state,
1062
+ ["images", imageIndex, "regions", articleMetadataRegionIdx],
1063
+ {
1064
+ ...articleRegionToUpdate,
1065
+ text: JSON.stringify(articleMetadata)
1066
+ }
1067
+ )
1068
+ }
1069
+
1070
+ // update metadata of image
1071
+ const metadataIndex = state.images[imageIndex]?.metadata?.findIndex(mt => mt.key === name)
1072
+ if (metadataIndex < 0) {
1073
+ console.error(`can't find photo metadata by key "${name}"`)
1074
+ return state
1075
+ }
1076
+ state = addSaveableAction(state, "UPDATE_METADATA")
1077
+ return setIn(
1078
+ state,
1079
+ ["images", imageIndex, "metadata", metadataIndex],
1080
+ {
1081
+ key: name,
1082
+ value: value
1083
+ }
1084
+ )
1085
+ }
1086
+ }
1087
+ case "ADD_GROUP": {
1088
+ // FIXME: remove this branch
1089
+ return state
1090
+ // const { group } = action;
1091
+ // const newAllowedGroups = []
1092
+ // return setIn(
1093
+ // state,
1094
+ // ["allowedGroups"],
1095
+ // newAllowedGroups
1096
+ // )
1097
+ }
1098
+ case "SAVE_IMAGE": {
1099
+ const { image, triggerRecalc, toSaveMetadata } = action;
1100
+ return setIn(state, ["toSaveImage"], { image, triggerRecalc, toSaveMetadata });
1101
+ }
1102
+ case "RECALC_CLICKED": {
1103
+ const { imageId } = action;
1104
+ const imageIdx = state.images.findIndex(i => i.id === imageId);
1105
+ if (imageIdx < 0) {
1106
+ throw new Error(`failed to find index of image with id ${imageId}`)
1107
+ }
1108
+ const image = { ...state.images[imageIdx] }
1109
+ return setIn(state, ["toSaveImage"], { image, triggerRecalc: true, toSaveMetadata: [] });
1110
+ }
1111
+ case "IMAGE_UPDATE_INIT": {
1112
+ const { imageId } = action;
1113
+ const imageIdx = state.images.findIndex(i => i.id === imageId);
1114
+ if (imageIdx < 0) {
1115
+ throw new Error(`failed to find index of image with id ${imageId}`)
1116
+ }
1117
+
1118
+ state = setIn(state, ["toSaveImage"], null);
1119
+
1120
+ return setIn(
1121
+ state,
1122
+ ["images", imageIdx],
1123
+ {
1124
+ ...state.images[imageIdx],
1125
+ lockedUntil: defaultLockedUntil(),
1126
+ syncError: null,
1127
+ saveableActions: [],
1128
+ }
1129
+ )
1130
+ }
1131
+ case "IMAGE_UPDATE_SUCCESS": {
1132
+ const { imageId, lockedUntil } = action;
1133
+ const imageIdx = state.images.findIndex(i => i.id === imageId);
1134
+ if (imageIdx < 0) {
1135
+ throw new Error(`failed to find index of image with id ${imageId}`)
1136
+ }
1137
+
1138
+ // if there is lockedUntil set, it means we need to poll for updated
1139
+ if (lockedUntil) {
1140
+ state = setIn(state, ["toPollImages"], uniq([...state.toPollImages, imageId]))
1141
+ }
1142
+
1143
+ return setIn(
1144
+ state,
1145
+ ["images", imageIdx],
1146
+ {
1147
+ ...state.images[imageIdx],
1148
+ lockedUntil,
1149
+ syncError: null
1150
+ }
1151
+ )
1152
+ }
1153
+ case "IMAGE_UPDATE_FAIL": {
1154
+ const { imageId, error } = action;
1155
+ const imageIdx = state.images.findIndex(i => i.id === imageId);
1156
+ if (imageIdx < 0) {
1157
+ throw new Error(`failed to find index of image with id ${imageId}`)
1158
+ }
1159
+
1160
+ return setIn(
1161
+ state,
1162
+ ["images", imageIdx],
1163
+ {
1164
+ ...state.images[imageIdx],
1165
+ lockedUntil: null,
1166
+ syncError: error
1167
+ }
1168
+ )
1169
+ }
1170
+ case "IMAGE_POLL_INIT": {
1171
+ const { imageIds } = action;
1172
+ return setIn(state, ["toPollImages"], state.toPollImages.filter(i => !imageIds.includes(i)))
1173
+ }
1174
+ case "IMAGE_POLL_SUCCESS": {
1175
+ const { image } = action;
1176
+ const imageIdx = state.images.findIndex(i => i.id === image.id);
1177
+ if (imageIdx < 0) {
1178
+ throw new Error(`failed to find index of image with id ${image.id}`)
1179
+ }
1180
+
1181
+ return setIn(
1182
+ state,
1183
+ ["images", imageIdx],
1184
+ {
1185
+ ...state.images[imageIdx],
1186
+ lockedUntil: null,
1187
+ syncError: null,
1188
+ ...image
1189
+ }
1190
+ )
1191
+ }
1192
+ case "IMAGE_POLL_TIMEOUT": {
1193
+ const { imageId } = action;
1194
+ const imageIdx = state.images.findIndex(i => i.id === imageId);
1195
+ if (imageIdx < 0) {
1196
+ throw new Error(`failed to find index of image with id ${imageId}`)
1197
+ }
1198
+
1199
+ return setIn(
1200
+ state,
1201
+ ["images", imageIdx],
1202
+ {
1203
+ ...state.images[imageIdx],
1204
+ lockedUntil: null,
1205
+ syncError: new Error(`polling timeout`)
1206
+ }
1207
+ )
1208
+ }
1209
+ case "ZOOM_RESET":
1210
+ {
1211
+ if (state.selectedImage === undefined || state.selectedImage === null) {
1212
+ return state
1213
+ }
1214
+
1215
+ const deselectedRegions = state.images[state.selectedImage].regions.map(i => ({
1216
+ ...i,
1217
+ highlighted: false,
1218
+ groupHighlighted: false
1219
+ }));
1220
+
1221
+ state = setIn(state, ["selectedGroupId"], null);
1222
+ return setIn(state, ["images", state.selectedImage, "regions"], deselectedRegions);
1223
+ }
1224
+ default:
1225
+ break
1226
+ }
1227
+ return state
1228
+ }