@searpent/react-image-annotate 2.0.0

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 (109) 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/README.md +101 -0
  12. package/package.json +93 -0
  13. package/public/favicon.ico +0 -0
  14. package/public/index.html +38 -0
  15. package/src/Annotator/bike-pic.png +0 -0
  16. package/src/Annotator/bike-pic2.png +0 -0
  17. package/src/Annotator/dab-keyframes.story.json +1 -0
  18. package/src/Annotator/index.js +206 -0
  19. package/src/Annotator/index.story.js +727 -0
  20. package/src/Annotator/poses.story.js +150 -0
  21. package/src/Annotator/reducers/combine-reducers.js +7 -0
  22. package/src/Annotator/reducers/convert-expanding-line-to-polygon.js +53 -0
  23. package/src/Annotator/reducers/fix-twisted.js +6 -0
  24. package/src/Annotator/reducers/general-reducer.js +914 -0
  25. package/src/Annotator/reducers/get-active-image.js +21 -0
  26. package/src/Annotator/reducers/get-implied-video-regions.js +115 -0
  27. package/src/Annotator/reducers/history-handler.js +60 -0
  28. package/src/Annotator/reducers/image-reducer.js +23 -0
  29. package/src/Annotator/reducers/video-reducer.js +85 -0
  30. package/src/Annotator/video.story.js +51 -0
  31. package/src/ClassSelectionMenu/index.js +108 -0
  32. package/src/Crosshairs/index.js +64 -0
  33. package/src/DebugSidebarBox/index.js +36 -0
  34. package/src/DemoSite/Editor.js +235 -0
  35. package/src/DemoSite/ErrorBoundaryDialog.js +34 -0
  36. package/src/DemoSite/index.js +41 -0
  37. package/src/DemoSite/index.story.js +10 -0
  38. package/src/DemoSite/simple-segmentation-example.json +572 -0
  39. package/src/FullImageSegmentationAnnotator/hard1.story.jpg +0 -0
  40. package/src/FullImageSegmentationAnnotator/hard2.story.jpg +0 -0
  41. package/src/FullImageSegmentationAnnotator/hard3.story.jpg +0 -0
  42. package/src/FullImageSegmentationAnnotator/index.js +7 -0
  43. package/src/FullImageSegmentationAnnotator/index.story.js +177 -0
  44. package/src/FullImageSegmentationAnnotator/orange.story.png +0 -0
  45. package/src/HighlightBox/index.js +143 -0
  46. package/src/HistorySidebarBox/index.js +78 -0
  47. package/src/ImageCanvas/dancing-man.story.jpg +0 -0
  48. package/src/ImageCanvas/index.js +488 -0
  49. package/src/ImageCanvas/index.story.js +214 -0
  50. package/src/ImageCanvas/mouse_mask.story.png +0 -0
  51. package/src/ImageCanvas/region-tools.js +171 -0
  52. package/src/ImageCanvas/seves_desk.story.jpg +0 -0
  53. package/src/ImageCanvas/styles.js +27 -0
  54. package/src/ImageCanvas/use-mouse.js +168 -0
  55. package/src/ImageCanvas/use-project-box.js +23 -0
  56. package/src/ImageCanvas/use-wasd-mode.js +50 -0
  57. package/src/ImageMask/index.js +127 -0
  58. package/src/ImageMask/load-image.js +32 -0
  59. package/src/ImageSelectorSidebarBox/index.js +54 -0
  60. package/src/KeyframeTimeline/get-time-string.js +25 -0
  61. package/src/KeyframeTimeline/index.js +223 -0
  62. package/src/KeyframesSelectorSidebarBox/index.js +93 -0
  63. package/src/LandingPage/content.md +57 -0
  64. package/src/LandingPage/github-markdown.css +964 -0
  65. package/src/LandingPage/index.js +147 -0
  66. package/src/MainLayout/icon-dictionary.js +79 -0
  67. package/src/MainLayout/index.js +420 -0
  68. package/src/MainLayout/index.story.js +240 -0
  69. package/src/MainLayout/styles.js +26 -0
  70. package/src/MainLayout/types.js +156 -0
  71. package/src/MainLayout/use-implied-video-regions.js +17 -0
  72. package/src/PointDistances/index.js +90 -0
  73. package/src/PreventScrollToParents/index.js +48 -0
  74. package/src/PreventScrollToParents/index.story.js +23 -0
  75. package/src/RegionLabel/index.js +201 -0
  76. package/src/RegionLabel/styles.js +51 -0
  77. package/src/RegionSelectAndTransformBoxes/index.js +234 -0
  78. package/src/RegionSelectorSidebarBox/index.js +216 -0
  79. package/src/RegionSelectorSidebarBox/styles.js +54 -0
  80. package/src/RegionShapes/index.js +236 -0
  81. package/src/RegionTags/index.js +130 -0
  82. package/src/SettingsDialog/index.js +58 -0
  83. package/src/SettingsProvider/index.js +44 -0
  84. package/src/Shortcuts/ShortcutField.js +44 -0
  85. package/src/Shortcuts/index.js +129 -0
  86. package/src/ShortcutsManager/index.js +162 -0
  87. package/src/Sidebar/index.js +117 -0
  88. package/src/SidebarBoxContainer/index.js +93 -0
  89. package/src/SmallToolButton/index.js +57 -0
  90. package/src/TagsSidebarBox/index.js +93 -0
  91. package/src/TaskDescriptionSidebarBox/index.js +43 -0
  92. package/src/Theme/index.js +36 -0
  93. package/src/VideoOrImageCanvasBackground/index.js +170 -0
  94. package/src/colors.js +32 -0
  95. package/src/hooks/use-event-callback.js +11 -0
  96. package/src/hooks/use-exclude-pattern.js +27 -0
  97. package/src/hooks/use-load-image.js +21 -0
  98. package/src/hooks/use-window-size.js +46 -0
  99. package/src/hooks/xpattern.js +1 -0
  100. package/src/hooks/xpattern.png +0 -0
  101. package/src/index.js +18 -0
  102. package/src/lib.js +7 -0
  103. package/src/screenshot.png +0 -0
  104. package/src/site.css +5 -0
  105. package/src/stories.js +2 -0
  106. package/src/utils/get-from-local-storage.js +7 -0
  107. package/src/utils/get-hotkey-help-text.js +11 -0
  108. package/src/utils/get-landmarks-with-transform.js +23 -0
  109. package/src/utils/set-in-local-storage.js +6 -0
@@ -0,0 +1,914 @@
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 getActiveImage from "./get-active-image"
8
+ import { saveToHistory } from "./history-handler.js"
9
+ import colors from "../../colors"
10
+ import fixTwisted from "./fix-twisted"
11
+ import convertExpandingLineToPolygon from "./convert-expanding-line-to-polygon"
12
+ import clamp from "clamp"
13
+ import getLandmarksWithTransform from "../../utils/get-landmarks-with-transform"
14
+ import setInLocalStorage from "../../utils/set-in-local-storage"
15
+
16
+ const getRandomId = () => Math.random().toString().split(".")[1]
17
+
18
+ export default (state: MainLayoutState, action: Action) => {
19
+ if (
20
+ state.allowedArea &&
21
+ state.selectedTool !== "modify-allowed-area" &&
22
+ ["MOUSE_DOWN", "MOUSE_UP", "MOUSE_MOVE"].includes(action.type)
23
+ ) {
24
+ const aa = state.allowedArea
25
+ action.x = clamp(action.x, aa.x, aa.x + aa.w)
26
+ action.y = clamp(action.y, aa.y, aa.y + aa.h)
27
+ }
28
+
29
+ if (action.type === "ON_CLS_ADDED" && action.cls && action.cls !== "") {
30
+ const oldRegionClsList = state.regionClsList
31
+ const newState = {
32
+ ...state,
33
+ regionClsList: oldRegionClsList.concat(action.cls),
34
+ }
35
+ return newState
36
+ }
37
+
38
+ // Throttle certain actions
39
+ if (action.type === "MOUSE_MOVE") {
40
+ if (Date.now() - ((state: any).lastMouseMoveCall || 0) < 16) return state
41
+ state = setIn(state, ["lastMouseMoveCall"], Date.now())
42
+ }
43
+ if (!action.type.includes("MOUSE")) {
44
+ state = setIn(state, ["lastAction"], action)
45
+ }
46
+
47
+ const { currentImageIndex, pathToActiveImage, activeImage } =
48
+ getActiveImage(state)
49
+
50
+ const getRegionIndex = (region) => {
51
+ const regionId =
52
+ typeof region === "string" || typeof region === "number"
53
+ ? region
54
+ : region.id
55
+ if (!activeImage) return null
56
+ const regionIndex = (activeImage.regions || []).findIndex(
57
+ (r) => r.id === regionId
58
+ )
59
+ return regionIndex === -1 ? null : regionIndex
60
+ }
61
+ const getRegion = (regionId) => {
62
+ if (!activeImage) return null
63
+ const regionIndex = getRegionIndex(regionId)
64
+ if (regionIndex === null) return [null, null]
65
+ const region = activeImage.regions[regionIndex]
66
+ return [region, regionIndex]
67
+ }
68
+ const modifyRegion = (regionId, obj) => {
69
+ const [region, regionIndex] = getRegion(regionId)
70
+ if (!region) return state
71
+ if (obj !== null) {
72
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
73
+ ...region,
74
+ ...obj,
75
+ })
76
+ } else {
77
+ // delete region
78
+ const regions = activeImage.regions
79
+ return setIn(
80
+ state,
81
+ [...pathToActiveImage, "regions"],
82
+ (regions || []).filter((r) => r.id !== region.id)
83
+ )
84
+ }
85
+ }
86
+ const unselectRegions = (state: MainLayoutState) => {
87
+ if (!activeImage) return state
88
+ return setIn(
89
+ state,
90
+ [...pathToActiveImage, "regions"],
91
+ (activeImage.regions || []).map((r) => ({
92
+ ...r,
93
+ highlighted: false,
94
+ }))
95
+ )
96
+ }
97
+
98
+ const closeEditors = (state: MainLayoutState) => {
99
+ if (currentImageIndex === null) return state
100
+ return setIn(
101
+ state,
102
+ [...pathToActiveImage, "regions"],
103
+ (activeImage.regions || []).map((r) => ({
104
+ ...r,
105
+ editingLabels: false,
106
+ }))
107
+ )
108
+ }
109
+
110
+ const setNewImage = (img: string | Object, index: number) => {
111
+ let { src, frameTime } = typeof img === "object" ? img : { src: img }
112
+ return setIn(
113
+ setIn(state, ["selectedImage"], index),
114
+ ["selectedImageFrameTime"],
115
+ frameTime
116
+ )
117
+ }
118
+
119
+ switch (action.type) {
120
+ case "@@INIT": {
121
+ return state
122
+ }
123
+ case "SELECT_IMAGE": {
124
+ return setNewImage(action.image, action.imageIndex)
125
+ }
126
+ case "SELECT_CLASSIFICATION": {
127
+ return setIn(state, ["selectedCls"], action.cls)
128
+ }
129
+ case "CHANGE_REGION": {
130
+ const regionIndex = getRegionIndex(action.region)
131
+ if (regionIndex === null) return state
132
+ const oldRegion = activeImage.regions[regionIndex]
133
+ if (oldRegion.cls !== action.region.cls) {
134
+ state = saveToHistory(state, "Change Region Classification")
135
+ const clsIndex = state.regionClsList.indexOf(action.region.cls)
136
+ if (clsIndex !== -1) {
137
+ state = setIn(state, ["selectedCls"], action.region.cls)
138
+ action.region.color = colors[clsIndex % colors.length]
139
+ }
140
+ }
141
+ if (!isEqual(oldRegion.tags, action.region.tags)) {
142
+ state = saveToHistory(state, "Change Region Tags")
143
+ }
144
+ if (!isEqual(oldRegion.comment, action.region.comment)) {
145
+ state = saveToHistory(state, "Change Region Comment")
146
+ }
147
+ return setIn(
148
+ state,
149
+ [...pathToActiveImage, "regions", regionIndex],
150
+ action.region
151
+ )
152
+ }
153
+ case "CHANGE_IMAGE": {
154
+ if (!activeImage) return state
155
+ const { delta } = action
156
+ for (const key of Object.keys(delta)) {
157
+ if (key === "cls") saveToHistory(state, "Change Image Class")
158
+ if (key === "tags") saveToHistory(state, "Change Image Tags")
159
+ state = setIn(state, [...pathToActiveImage, key], delta[key])
160
+ }
161
+ return state
162
+ }
163
+ case "SELECT_REGION": {
164
+ const { region } = action
165
+ const regionIndex = getRegionIndex(action.region)
166
+ if (regionIndex === null) return state
167
+ const regions = [...(activeImage.regions || [])].map((r) => ({
168
+ ...r,
169
+ highlighted: r.id === region.id,
170
+ editingLabels: r.id === region.id,
171
+ }))
172
+ return setIn(state, [...pathToActiveImage, "regions"], regions)
173
+ }
174
+ case "BEGIN_MOVE_POINT": {
175
+ state = closeEditors(state)
176
+ return setIn(state, ["mode"], {
177
+ mode: "MOVE_REGION",
178
+ regionId: action.point.id,
179
+ })
180
+ }
181
+ case "BEGIN_BOX_TRANSFORM": {
182
+ const { box, directions } = action
183
+ state = closeEditors(state)
184
+ if (directions[0] === 0 && directions[1] === 0) {
185
+ return setIn(state, ["mode"], { mode: "MOVE_REGION", regionId: box.id })
186
+ } else {
187
+ return setIn(state, ["mode"], {
188
+ mode: "RESIZE_BOX",
189
+ regionId: box.id,
190
+ freedom: directions,
191
+ original: { x: box.x, y: box.y, w: box.w, h: box.h },
192
+ })
193
+ }
194
+ }
195
+ case "BEGIN_MOVE_POLYGON_POINT": {
196
+ const { polygon, pointIndex } = action
197
+ state = closeEditors(state)
198
+ if (
199
+ state.mode &&
200
+ state.mode.mode === "DRAW_POLYGON" &&
201
+ pointIndex === 0
202
+ ) {
203
+ return setIn(
204
+ modifyRegion(polygon, {
205
+ points: polygon.points.slice(0, -1),
206
+ open: false,
207
+ }),
208
+ ["mode"],
209
+ null
210
+ )
211
+ } else {
212
+ state = saveToHistory(state, "Move Polygon Point")
213
+ }
214
+ return setIn(state, ["mode"], {
215
+ mode: "MOVE_POLYGON_POINT",
216
+ regionId: polygon.id,
217
+ pointIndex,
218
+ })
219
+ }
220
+ case "BEGIN_MOVE_KEYPOINT": {
221
+ const { region, keypointId } = action
222
+ state = closeEditors(state)
223
+ state = saveToHistory(state, "Move Keypoint")
224
+ return setIn(state, ["mode"], {
225
+ mode: "MOVE_KEYPOINT",
226
+ regionId: region.id,
227
+ keypointId,
228
+ })
229
+ }
230
+ case "ADD_POLYGON_POINT": {
231
+ const { polygon, point, pointIndex } = action
232
+ const regionIndex = getRegionIndex(polygon)
233
+ if (regionIndex === null) return state
234
+ const points = [...polygon.points]
235
+ points.splice(pointIndex, 0, point)
236
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
237
+ ...polygon,
238
+ points,
239
+ })
240
+ }
241
+ case "MOUSE_MOVE": {
242
+ const { x, y } = action
243
+
244
+ if (!state.mode) return state
245
+ if (!activeImage) return state
246
+ const { mouseDownAt } = state
247
+ switch (state.mode.mode) {
248
+ case "MOVE_POLYGON_POINT": {
249
+ const { pointIndex, regionId } = state.mode
250
+ const regionIndex = getRegionIndex(regionId)
251
+ if (regionIndex === null) return state
252
+ return setIn(
253
+ state,
254
+ [
255
+ ...pathToActiveImage,
256
+ "regions",
257
+ regionIndex,
258
+ "points",
259
+ pointIndex,
260
+ ],
261
+ [x, y]
262
+ )
263
+ }
264
+ case "MOVE_KEYPOINT": {
265
+ const { keypointId, regionId } = state.mode
266
+ const [region, regionIndex] = getRegion(regionId)
267
+ if (regionIndex === null) return state
268
+ return setIn(
269
+ state,
270
+ [
271
+ ...pathToActiveImage,
272
+ "regions",
273
+ regionIndex,
274
+ "points",
275
+ keypointId,
276
+ ],
277
+ { ...(region: any).points[keypointId], x, y }
278
+ )
279
+ }
280
+ case "MOVE_REGION": {
281
+ const { regionId } = state.mode
282
+ if (regionId === "$$allowed_area") {
283
+ const {
284
+ allowedArea: { w, h },
285
+ } = state
286
+ return setIn(state, ["allowedArea"], {
287
+ x: x - w / 2,
288
+ y: y - h / 2,
289
+ w,
290
+ h,
291
+ })
292
+ }
293
+ const regionIndex = getRegionIndex(regionId)
294
+ if (regionIndex === null) return state
295
+ return setIn(
296
+ state,
297
+ [...pathToActiveImage, "regions", regionIndex],
298
+ moveRegion(activeImage.regions[regionIndex], x, y)
299
+ )
300
+ }
301
+ case "RESIZE_BOX": {
302
+ const {
303
+ regionId,
304
+ freedom: [xFree, yFree],
305
+ original: { x: ox, y: oy, w: ow, h: oh },
306
+ } = state.mode
307
+
308
+ const dx = xFree === 0 ? ox : xFree === -1 ? Math.min(ox + ow, x) : ox
309
+ const dw =
310
+ xFree === 0
311
+ ? ow
312
+ : xFree === -1
313
+ ? ow + (ox - dx)
314
+ : Math.max(0, ow + (x - ox - ow))
315
+ const dy = yFree === 0 ? oy : yFree === -1 ? Math.min(oy + oh, y) : oy
316
+ const dh =
317
+ yFree === 0
318
+ ? oh
319
+ : yFree === -1
320
+ ? oh + (oy - dy)
321
+ : Math.max(0, oh + (y - oy - oh))
322
+
323
+ // determine if we should switch the freedom
324
+ if (dw <= 0.001) {
325
+ state = setIn(state, ["mode", "freedom"], [xFree * -1, yFree])
326
+ }
327
+ if (dh <= 0.001) {
328
+ state = setIn(state, ["mode", "freedom"], [xFree, yFree * -1])
329
+ }
330
+
331
+ if (regionId === "$$allowed_area") {
332
+ return setIn(state, ["allowedArea"], {
333
+ x: dx,
334
+ w: dw,
335
+ y: dy,
336
+ h: dh,
337
+ })
338
+ }
339
+
340
+ const regionIndex = getRegionIndex(regionId)
341
+ if (regionIndex === null) return state
342
+ const box = activeImage.regions[regionIndex]
343
+
344
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
345
+ ...box,
346
+ x: dx,
347
+ w: dw,
348
+ y: dy,
349
+ h: dh,
350
+ })
351
+ }
352
+ case "RESIZE_KEYPOINTS": {
353
+ const { regionId, landmarks, centerX, centerY } = state.mode
354
+ const distFromCenter = Math.sqrt(
355
+ (centerX - x) ** 2 + (centerY - y) ** 2
356
+ )
357
+ const scale = distFromCenter / 0.15
358
+ return modifyRegion(regionId, {
359
+ points: getLandmarksWithTransform({
360
+ landmarks,
361
+ center: { x: centerX, y: centerY },
362
+ scale,
363
+ }),
364
+ })
365
+ }
366
+ case "DRAW_POLYGON": {
367
+ const { regionId } = state.mode
368
+ const [region, regionIndex] = getRegion(regionId)
369
+ if (!region) return setIn(state, ["mode"], null)
370
+ return setIn(
371
+ state,
372
+ [
373
+ ...pathToActiveImage,
374
+ "regions",
375
+ regionIndex,
376
+ "points",
377
+ (region: any).points.length - 1,
378
+ ],
379
+ [x, y]
380
+ )
381
+ }
382
+ case "DRAW_LINE": {
383
+ const { regionId } = state.mode
384
+ const [region, regionIndex] = getRegion(regionId)
385
+ if (!region) return setIn(state, ["mode"], null)
386
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
387
+ ...region,
388
+ x2: x,
389
+ y2: y,
390
+ })
391
+ }
392
+ case "DRAW_EXPANDING_LINE": {
393
+ const { regionId } = state.mode
394
+ const [expandingLine, regionIndex] = getRegion(regionId)
395
+ if (!expandingLine) return state
396
+ const isMouseDown = Boolean(state.mouseDownAt)
397
+ if (isMouseDown) {
398
+ // If the mouse is down, set width/angle
399
+ const lastPoint = expandingLine.points.slice(-1)[0]
400
+ const mouseDistFromLastPoint = Math.sqrt(
401
+ (lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2
402
+ )
403
+ if (mouseDistFromLastPoint < 0.002 && !lastPoint.width) return state
404
+
405
+ const newState = setIn(
406
+ state,
407
+ [...pathToActiveImage, "regions", regionIndex, "points"],
408
+ expandingLine.points.slice(0, -1).concat([
409
+ {
410
+ ...lastPoint,
411
+ width: mouseDistFromLastPoint * 2,
412
+ angle: Math.atan2(lastPoint.x - x, lastPoint.y - y),
413
+ },
414
+ ])
415
+ )
416
+ return newState
417
+ } else {
418
+ // If mouse is up, move the next candidate point
419
+ return setIn(
420
+ state,
421
+ [...pathToActiveImage, "regions", regionIndex],
422
+ {
423
+ ...expandingLine,
424
+ candidatePoint: { x, y },
425
+ }
426
+ )
427
+ }
428
+
429
+ return state
430
+ }
431
+ case "SET_EXPANDING_LINE_WIDTH": {
432
+ const { regionId } = state.mode
433
+ const [expandingLine, regionIndex] = getRegion(regionId)
434
+ if (!expandingLine) return state
435
+ const lastPoint = expandingLine.points.slice(-1)[0]
436
+ const { mouseDownAt } = state
437
+ return setIn(
438
+ state,
439
+ [...pathToActiveImage, "regions", regionIndex, "expandingWidth"],
440
+ Math.sqrt((lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2)
441
+ )
442
+ }
443
+ default:
444
+ return state
445
+ }
446
+ }
447
+ case "MOUSE_DOWN": {
448
+ if (!activeImage) return state
449
+ const { x, y } = action
450
+
451
+ state = setIn(state, ["mouseDownAt"], { x, y })
452
+
453
+ if (state.mode) {
454
+ switch (state.mode.mode) {
455
+ case "DRAW_POLYGON": {
456
+ const [polygon, regionIndex] = getRegion(state.mode.regionId)
457
+ if (!polygon) break
458
+ return setIn(
459
+ state,
460
+ [...pathToActiveImage, "regions", regionIndex],
461
+ { ...polygon, points: polygon.points.concat([[x, y]]) }
462
+ )
463
+ }
464
+ case "DRAW_LINE": {
465
+ const [line, regionIndex] = getRegion(state.mode.regionId)
466
+ if (!line) break
467
+ setIn(state, [...pathToActiveImage, "regions", regionIndex], {
468
+ ...line,
469
+ x2: x,
470
+ y2: y,
471
+ })
472
+ return setIn(state, ["mode"], null)
473
+ }
474
+ case "DRAW_EXPANDING_LINE": {
475
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
476
+ if (!expandingLine) break
477
+ const lastPoint = expandingLine.points.slice(-1)[0]
478
+ if (
479
+ expandingLine.points.length > 1 &&
480
+ Math.sqrt((lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2) < 0.002
481
+ ) {
482
+ if (!lastPoint.width) {
483
+ return setIn(state, ["mode"], {
484
+ mode: "SET_EXPANDING_LINE_WIDTH",
485
+ regionId: state.mode.regionId,
486
+ })
487
+ } else {
488
+ return state
489
+ .setIn(
490
+ [...pathToActiveImage, "regions", regionIndex],
491
+ convertExpandingLineToPolygon(expandingLine)
492
+ )
493
+ .setIn(["mode"], null)
494
+ }
495
+ }
496
+
497
+ // Create new point
498
+ return setIn(
499
+ state,
500
+ [...pathToActiveImage, "regions", regionIndex, "points"],
501
+ expandingLine.points.concat([{ x, y, angle: null, width: null }])
502
+ )
503
+ }
504
+ case "SET_EXPANDING_LINE_WIDTH": {
505
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
506
+ if (!expandingLine) break
507
+ const { expandingWidth } = expandingLine
508
+ return state
509
+ .setIn(
510
+ [...pathToActiveImage, "regions", regionIndex],
511
+ convertExpandingLineToPolygon({
512
+ ...expandingLine,
513
+ points: expandingLine.points.map((p) =>
514
+ p.width ? p : { ...p, width: expandingWidth }
515
+ ),
516
+ expandingWidth: undefined,
517
+ })
518
+ )
519
+ .setIn(["mode"], null)
520
+ }
521
+ default:
522
+ break
523
+ }
524
+ }
525
+
526
+ let newRegion
527
+ let defaultRegionCls = state.selectedCls,
528
+ defaultRegionColor = "#ff0000"
529
+
530
+ const clsIndex = (state.regionClsList || []).indexOf(defaultRegionCls)
531
+ if (clsIndex !== -1) {
532
+ defaultRegionColor = colors[clsIndex % colors.length]
533
+ }
534
+
535
+ switch (state.selectedTool) {
536
+ case "create-point": {
537
+ state = saveToHistory(state, "Create Point")
538
+ newRegion = {
539
+ type: "point",
540
+ x,
541
+ y,
542
+ highlighted: true,
543
+ editingLabels: true,
544
+ color: defaultRegionColor,
545
+ id: getRandomId(),
546
+ cls: defaultRegionCls,
547
+ }
548
+ break
549
+ }
550
+ case "create-box": {
551
+ state = saveToHistory(state, "Create Box")
552
+ newRegion = {
553
+ type: "box",
554
+ x: x,
555
+ y: y,
556
+ w: 0,
557
+ h: 0,
558
+ highlighted: true,
559
+ editingLabels: false,
560
+ color: defaultRegionColor,
561
+ cls: defaultRegionCls,
562
+ id: getRandomId(),
563
+ }
564
+ state = setIn(state, ["mode"], {
565
+ mode: "RESIZE_BOX",
566
+ editLabelEditorAfter: true,
567
+ regionId: newRegion.id,
568
+ freedom: [1, 1],
569
+ original: { x, y, w: newRegion.w, h: newRegion.h },
570
+ isNew: true,
571
+ })
572
+ break
573
+ }
574
+ case "create-polygon": {
575
+ if (state.mode && state.mode.mode === "DRAW_POLYGON") break
576
+ state = saveToHistory(state, "Create Polygon")
577
+ newRegion = {
578
+ type: "polygon",
579
+ points: [
580
+ [x, y],
581
+ [x, y],
582
+ ],
583
+ open: true,
584
+ highlighted: true,
585
+ color: defaultRegionColor,
586
+ cls: defaultRegionCls,
587
+ id: getRandomId(),
588
+ }
589
+ state = setIn(state, ["mode"], {
590
+ mode: "DRAW_POLYGON",
591
+ regionId: newRegion.id,
592
+ })
593
+ break
594
+ }
595
+ case "create-expanding-line": {
596
+ state = saveToHistory(state, "Create Expanding Line")
597
+ newRegion = {
598
+ type: "expanding-line",
599
+ unfinished: true,
600
+ points: [{ x, y, angle: null, width: null }],
601
+ open: true,
602
+ highlighted: true,
603
+ color: defaultRegionColor,
604
+ cls: defaultRegionCls,
605
+ id: getRandomId(),
606
+ }
607
+ state = setIn(state, ["mode"], {
608
+ mode: "DRAW_EXPANDING_LINE",
609
+ regionId: newRegion.id,
610
+ })
611
+ break
612
+ }
613
+ case "create-line": {
614
+ if (state.mode && state.mode.mode === "DRAW_LINE") break
615
+ state = saveToHistory(state, "Create Line")
616
+ newRegion = {
617
+ type: "line",
618
+ x1: x,
619
+ y1: y,
620
+ x2: x,
621
+ y2: y,
622
+ highlighted: true,
623
+ editingLabels: false,
624
+ color: defaultRegionColor,
625
+ cls: defaultRegionCls,
626
+ id: getRandomId(),
627
+ }
628
+ state = setIn(state, ["mode"], {
629
+ mode: "DRAW_LINE",
630
+ regionId: newRegion.id,
631
+ })
632
+ break
633
+ }
634
+ case "create-keypoints": {
635
+ state = saveToHistory(state, "Create Keypoints")
636
+ const [[keypointsDefinitionId, { landmarks, connections }]] =
637
+ (Object.entries(state.keypointDefinitions): any)
638
+
639
+ newRegion = {
640
+ type: "keypoints",
641
+ keypointsDefinitionId,
642
+ points: getLandmarksWithTransform({
643
+ landmarks,
644
+ center: { x, y },
645
+ scale: 1,
646
+ }),
647
+ highlighted: true,
648
+ editingLabels: false,
649
+ id: getRandomId(),
650
+ }
651
+ state = setIn(state, ["mode"], {
652
+ mode: "RESIZE_KEYPOINTS",
653
+ landmarks,
654
+ centerX: x,
655
+ centerY: y,
656
+ regionId: newRegion.id,
657
+ isNew: true,
658
+ })
659
+ break
660
+ }
661
+ default:
662
+ break
663
+ }
664
+
665
+ const regions = [...(getIn(state, pathToActiveImage).regions || [])]
666
+ .map((r) =>
667
+ setIn(r, ["editingLabels"], false).setIn(["highlighted"], false)
668
+ )
669
+ .concat(newRegion ? [newRegion] : [])
670
+
671
+ return setIn(state, [...pathToActiveImage, "regions"], regions)
672
+ }
673
+ case "MOUSE_UP": {
674
+ const { x, y } = action
675
+
676
+ const { mouseDownAt = { x, y } } = state
677
+ if (!state.mode) return state
678
+ state = setIn(state, ["mouseDownAt"], null)
679
+ switch (state.mode.mode) {
680
+ case "RESIZE_BOX": {
681
+ if (state.mode.isNew) {
682
+ if (
683
+ Math.abs(state.mode.original.x - x) < 0.002 ||
684
+ Math.abs(state.mode.original.y - y) < 0.002
685
+ ) {
686
+ return setIn(
687
+ modifyRegion(state.mode.regionId, null),
688
+ ["mode"],
689
+ null
690
+ )
691
+ }
692
+ }
693
+ if (state.mode.editLabelEditorAfter) {
694
+ return {
695
+ ...modifyRegion(state.mode.regionId, { editingLabels: true }),
696
+ mode: null,
697
+ }
698
+ }
699
+ }
700
+ case "MOVE_REGION":
701
+ case "RESIZE_KEYPOINTS":
702
+ case "MOVE_POLYGON_POINT": {
703
+ return { ...state, mode: null }
704
+ }
705
+ case "MOVE_KEYPOINT": {
706
+ return { ...state, mode: null }
707
+ }
708
+ case "CREATE_POINT_LINE": {
709
+ return state
710
+ }
711
+ case "DRAW_EXPANDING_LINE": {
712
+ const [expandingLine, regionIndex] = getRegion(state.mode.regionId)
713
+ if (!expandingLine) return state
714
+ let newExpandingLine = expandingLine
715
+ const lastPoint =
716
+ expandingLine.points.length !== 0
717
+ ? expandingLine.points.slice(-1)[0]
718
+ : mouseDownAt
719
+ let jointStart
720
+ if (expandingLine.points.length > 1) {
721
+ jointStart = expandingLine.points.slice(-2)[0]
722
+ } else {
723
+ jointStart = lastPoint
724
+ }
725
+ const mouseDistFromLastPoint = Math.sqrt(
726
+ (lastPoint.x - x) ** 2 + (lastPoint.y - y) ** 2
727
+ )
728
+ if (mouseDistFromLastPoint > 0.002) {
729
+ // The user is drawing has drawn the width for the last point
730
+ const newPoints = [...expandingLine.points]
731
+ for (let i = 0; i < newPoints.length - 1; i++) {
732
+ if (newPoints[i].width) continue
733
+ newPoints[i] = {
734
+ ...newPoints[i],
735
+ width: lastPoint.width,
736
+ }
737
+ }
738
+ newExpandingLine = setIn(
739
+ expandingLine,
740
+ ["points"],
741
+ fixTwisted(newPoints)
742
+ )
743
+ } else {
744
+ return state
745
+ }
746
+ return setIn(
747
+ state,
748
+ [...pathToActiveImage, "regions", regionIndex],
749
+ newExpandingLine
750
+ )
751
+ }
752
+ default:
753
+ return state
754
+ }
755
+ }
756
+ case "OPEN_REGION_EDITOR": {
757
+ const { region } = action
758
+ const regionIndex = getRegionIndex(action.region)
759
+ if (regionIndex === null) return state
760
+ const newRegions = setIn(
761
+ activeImage.regions.map((r) => ({
762
+ ...r,
763
+ highlighted: false,
764
+ editingLabels: false,
765
+ })),
766
+ [regionIndex],
767
+ {
768
+ ...(activeImage.regions || [])[regionIndex],
769
+ highlighted: true,
770
+ editingLabels: true,
771
+ }
772
+ )
773
+ return setIn(state, [...pathToActiveImage, "regions"], newRegions)
774
+ }
775
+ case "CLOSE_REGION_EDITOR": {
776
+ const { region } = action
777
+ const regionIndex = getRegionIndex(action.region)
778
+ if (regionIndex === null) return state
779
+ return setIn(state, [...pathToActiveImage, "regions", regionIndex], {
780
+ ...(activeImage.regions || [])[regionIndex],
781
+ editingLabels: false,
782
+ })
783
+ }
784
+ case "DELETE_REGION": {
785
+ const regionIndex = getRegionIndex(action.region)
786
+ if (regionIndex === null) return state
787
+ return setIn(
788
+ state,
789
+ [...pathToActiveImage, "regions"],
790
+ (activeImage.regions || []).filter((r) => r.id !== action.region.id)
791
+ )
792
+ }
793
+ case "DELETE_SELECTED_REGION": {
794
+ return setIn(
795
+ state,
796
+ [...pathToActiveImage, "regions"],
797
+ (activeImage.regions || []).filter((r) => !r.highlighted)
798
+ )
799
+ }
800
+ case "HEADER_BUTTON_CLICKED": {
801
+ const buttonName = action.buttonName.toLowerCase()
802
+ switch (buttonName) {
803
+ case "prev": {
804
+ if (currentImageIndex === null) return state
805
+ if (currentImageIndex === 0) return state
806
+ return setNewImage(
807
+ state.images[currentImageIndex - 1],
808
+ currentImageIndex - 1
809
+ )
810
+ }
811
+ case "next": {
812
+ if (currentImageIndex === null) return state
813
+ if (currentImageIndex === state.images.length - 1) return state
814
+ return setNewImage(
815
+ state.images[currentImageIndex + 1],
816
+ currentImageIndex + 1
817
+ )
818
+ }
819
+ case "clone": {
820
+ if (currentImageIndex === null) return state
821
+ if (currentImageIndex === state.images.length - 1) return state
822
+ return setIn(
823
+ setNewImage(
824
+ state.images[currentImageIndex + 1],
825
+ currentImageIndex + 1
826
+ ),
827
+ ["images", currentImageIndex + 1, "regions"],
828
+ activeImage.regions
829
+ )
830
+ }
831
+ case "settings": {
832
+ return setIn(state, ["settingsOpen"], !state.settingsOpen)
833
+ }
834
+ case "help": {
835
+ return state
836
+ }
837
+ case "fullscreen": {
838
+ return setIn(state, ["fullScreen"], true)
839
+ }
840
+ case "exit fullscreen":
841
+ case "window": {
842
+ return setIn(state, ["fullScreen"], false)
843
+ }
844
+ case "hotkeys": {
845
+ return state
846
+ }
847
+ case "exit":
848
+ case "done": {
849
+ return state
850
+ }
851
+ default:
852
+ return state
853
+ }
854
+ }
855
+ case "SELECT_TOOL": {
856
+ if (action.selectedTool === "show-tags") {
857
+ setInLocalStorage("showTags", !state.showTags)
858
+ return setIn(state, ["showTags"], !state.showTags)
859
+ } else if (action.selectedTool === "show-mask") {
860
+ return setIn(state, ["showMask"], !state.showMask)
861
+ }
862
+ if (action.selectedTool === "modify-allowed-area" && !state.allowedArea) {
863
+ state = setIn(state, ["allowedArea"], { x: 0, y: 0, w: 1, h: 1 })
864
+ }
865
+ state = setIn(state, ["mode"], null)
866
+ return setIn(state, ["selectedTool"], action.selectedTool)
867
+ }
868
+ case "CANCEL": {
869
+ const { mode } = state
870
+ if (mode) {
871
+ switch (mode.mode) {
872
+ case "DRAW_EXPANDING_LINE":
873
+ case "SET_EXPANDING_LINE_WIDTH":
874
+ case "DRAW_POLYGON": {
875
+ const { regionId } = mode
876
+ return modifyRegion(regionId, null)
877
+ }
878
+ case "MOVE_POLYGON_POINT":
879
+ case "RESIZE_BOX":
880
+ case "MOVE_REGION": {
881
+ return setIn(state, ["mode"], null)
882
+ }
883
+ default:
884
+ return state
885
+ }
886
+ }
887
+ // Close any open boxes
888
+ const regions: any = activeImage.regions
889
+ if (regions && regions.some((r) => r.editingLabels)) {
890
+ return setIn(
891
+ state,
892
+ [...pathToActiveImage, "regions"],
893
+ regions.map((r) => ({
894
+ ...r,
895
+ editingLabels: false,
896
+ }))
897
+ )
898
+ } else if (regions) {
899
+ return setIn(
900
+ state,
901
+ [...pathToActiveImage, "regions"],
902
+ regions.map((r) => ({
903
+ ...r,
904
+ highlighted: false,
905
+ }))
906
+ )
907
+ }
908
+ break
909
+ }
910
+ default:
911
+ break
912
+ }
913
+ return state
914
+ }