@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,54 @@
1
+ import { grey, blue, orange, purple } from "@mui/material/colors"
2
+
3
+ export default {
4
+ container: {
5
+ fontSize: 11,
6
+ fontWeight: "bold",
7
+ color: grey[700],
8
+ "& .icon": {
9
+ marginTop: 4,
10
+ width: 16,
11
+ height: 16,
12
+ },
13
+ "& .icon2": {
14
+ opacity: 0.5,
15
+ width: 16,
16
+ height: 16,
17
+ transition: "200ms opacity",
18
+ "&:hover": {
19
+ cursor: "pointer",
20
+ opacity: 1,
21
+ },
22
+ },
23
+ },
24
+ row: {
25
+ padding: 4,
26
+ cursor: "pointer",
27
+ "&.header:hover": {
28
+ backgroundColor: "#fff",
29
+ },
30
+ "&.highlighted": {
31
+ backgroundColor: blue[100],
32
+ },
33
+ "&:hover": {
34
+ backgroundColor: blue[50],
35
+ color: grey[800],
36
+ },
37
+ },
38
+ chip: {
39
+ display: "flex",
40
+ flexDirection: "row",
41
+ padding: 2,
42
+ borderRadius: 2,
43
+ paddingLeft: 4,
44
+ paddingRight: 4,
45
+ alignItems: "center",
46
+ "& .color": {
47
+ borderRadius: 5,
48
+ width: 10,
49
+ height: 10,
50
+ marginRight: 4,
51
+ },
52
+ "& .text": {},
53
+ },
54
+ }
@@ -0,0 +1,236 @@
1
+ // @flow
2
+
3
+ import React, { memo } from "react"
4
+ import colorAlpha from "color-alpha"
5
+
6
+ function clamp(num, min, max) {
7
+ return num <= min ? min : num >= max ? max : num
8
+ }
9
+
10
+ const RegionComponents = {
11
+ point: memo(({ region, iw, ih }) => (
12
+ <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
13
+ <path
14
+ d={"M0 8L8 0L0 -8L-8 0Z"}
15
+ strokeWidth={2}
16
+ stroke={region.color}
17
+ fill="transparent"
18
+ />
19
+ </g>
20
+ )),
21
+ line: memo(({ region, iw, ih }) => (
22
+ <g transform={`translate(${region.x1 * iw} ${region.y1 * ih})`}>
23
+ <line
24
+ strokeWidth={2}
25
+ x1={0}
26
+ y1={0}
27
+ x2={(region.x2 - region.x1) * iw}
28
+ y2={(region.y2 - region.y1) * ih}
29
+ stroke={colorAlpha(region.color, 0.75)}
30
+ fill={colorAlpha(region.color, 0.25)}
31
+ />
32
+ </g>
33
+ )),
34
+ box: memo(({ region, iw, ih }) => (
35
+ <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
36
+ <rect
37
+ strokeWidth={2}
38
+ x={0}
39
+ y={0}
40
+ width={Math.max(region.w * iw, 0)}
41
+ height={Math.max(region.h * ih, 0)}
42
+ stroke={colorAlpha(region.color, 0.75)}
43
+ fill={colorAlpha(region.color, 0.25)}
44
+ />
45
+ </g>
46
+ )),
47
+ polygon: memo(({ region, iw, ih, fullSegmentationMode }) => {
48
+ const Component = region.open ? "polyline" : "polygon"
49
+ const alphaBase = fullSegmentationMode ? 0.5 : 1
50
+ return (
51
+ <Component
52
+ points={region.points
53
+ .map(([x, y]) => [x * iw, y * ih])
54
+ .map((a) => a.join(" "))
55
+ .join(" ")}
56
+ strokeWidth={2}
57
+ stroke={colorAlpha(region.color, 0.75)}
58
+ fill={colorAlpha(region.color, 0.25)}
59
+ />
60
+ )
61
+ }),
62
+ keypoints: ({ region, iw, ih, keypointDefinitions }) => {
63
+ const { points, keypointsDefinitionId } = region
64
+ if (!keypointDefinitions[keypointsDefinitionId]) {
65
+ throw new Error(
66
+ `No definition for keypoint configuration "${keypointsDefinitionId}"`
67
+ )
68
+ }
69
+ const { landmarks, connections } =
70
+ keypointDefinitions[keypointsDefinitionId]
71
+ return (
72
+ <g>
73
+ {Object.entries(points).map(([keypointId, { x, y }], i) => (
74
+ <g key={i} transform={`translate(${x * iw} ${y * ih})`}>
75
+ <path
76
+ d={"M0 8L8 0L0 -8L-8 0Z"}
77
+ strokeWidth={2}
78
+ stroke={landmarks[keypointId].color}
79
+ fill="transparent"
80
+ />
81
+ </g>
82
+ ))}
83
+ {connections.map(([kp1Id, kp2Id]) => {
84
+ const kp1 = points[kp1Id]
85
+ const kp2 = points[kp2Id]
86
+ const midPoint = { x: (kp1.x + kp2.x) / 2, y: (kp1.y + kp2.y) / 2 }
87
+
88
+ return (
89
+ <g key={`${kp1.x},${kp1.y}.${kp2.x},${kp2.y}`}>
90
+ <line
91
+ x1={kp1.x * iw}
92
+ y1={kp1.y * ih}
93
+ x2={midPoint.x * iw}
94
+ y2={midPoint.y * ih}
95
+ strokeWidth={2}
96
+ stroke={landmarks[kp1Id].color}
97
+ />
98
+ <line
99
+ x1={kp2.x * iw}
100
+ y1={kp2.y * ih}
101
+ x2={midPoint.x * iw}
102
+ y2={midPoint.y * ih}
103
+ strokeWidth={2}
104
+ stroke={landmarks[kp2Id].color}
105
+ />
106
+ </g>
107
+ )
108
+ })}
109
+ </g>
110
+ )
111
+ },
112
+ "expanding-line": memo(({ region, iw, ih }) => {
113
+ let { expandingWidth = 0.005, points } = region
114
+ expandingWidth = points.slice(-1)[0].width || expandingWidth
115
+ const pointPairs = points.map(({ x, y, angle, width }, i) => {
116
+ if (!angle) {
117
+ const n = points[clamp(i + 1, 0, points.length - 1)]
118
+ const p = points[clamp(i - 1, 0, points.length - 1)]
119
+ angle = Math.atan2(p.x - n.x, p.y - n.y) + Math.PI / 2
120
+ }
121
+ const dx = (Math.sin(angle) * (width || expandingWidth)) / 2
122
+ const dy = (Math.cos(angle) * (width || expandingWidth)) / 2
123
+ return [
124
+ { x: x + dx, y: y + dy },
125
+ { x: x - dx, y: y - dy },
126
+ ]
127
+ })
128
+ const firstSection = pointPairs.map(([p1, p2]) => p1)
129
+ const secondSection = pointPairs.map(([p1, p2]) => p2).asMutable()
130
+ secondSection.reverse()
131
+ const lastPoint = points.slice(-1)[0]
132
+ return (
133
+ <>
134
+ <polygon
135
+ points={firstSection
136
+ .concat(region.candidatePoint ? [region.candidatePoint] : [])
137
+ .concat(secondSection)
138
+ .map((p) => `${p.x * iw} ${p.y * ih}`)
139
+ .join(" ")}
140
+ strokeWidth={2}
141
+ stroke={colorAlpha(region.color, 0.75)}
142
+ fill={colorAlpha(region.color, 0.25)}
143
+ />
144
+ {points.map(({ x, y, angle }, i) => (
145
+ <g
146
+ key={i}
147
+ transform={`translate(${x * iw} ${y * ih}) rotate(${
148
+ (-(angle || 0) * 180) / Math.PI
149
+ })`}
150
+ >
151
+ <g>
152
+ <rect
153
+ x={-5}
154
+ y={-5}
155
+ width={10}
156
+ height={10}
157
+ strokeWidth={2}
158
+ stroke={colorAlpha(region.color, 0.75)}
159
+ fill={colorAlpha(region.color, 0.25)}
160
+ />
161
+ </g>
162
+ </g>
163
+ ))}
164
+ <rect
165
+ x={lastPoint.x * iw - 8}
166
+ y={lastPoint.y * ih - 8}
167
+ width={16}
168
+ height={16}
169
+ strokeWidth={4}
170
+ stroke={colorAlpha(region.color, 0.5)}
171
+ fill={"transparent"}
172
+ />
173
+ </>
174
+ )
175
+ }),
176
+ pixel: () => null,
177
+ }
178
+
179
+ export const WrappedRegionList = memo(
180
+ ({ regions, keypointDefinitions, iw, ih, fullSegmentationMode }) => {
181
+ return regions
182
+ .filter((r) => r.visible !== false)
183
+ .map((r) => {
184
+ const Component = RegionComponents[r.type]
185
+ return (
186
+ <Component
187
+ key={r.regionId}
188
+ region={r}
189
+ iw={iw}
190
+ ih={ih}
191
+ keypointDefinitions={keypointDefinitions}
192
+ fullSegmentationMode={fullSegmentationMode}
193
+ />
194
+ )
195
+ })
196
+ },
197
+ (n, p) => n.regions === p.regions && n.iw === p.iw && n.ih === p.ih
198
+ )
199
+
200
+ export const RegionShapes = ({
201
+ mat,
202
+ imagePosition,
203
+ regions = [],
204
+ keypointDefinitions,
205
+ fullSegmentationMode,
206
+ }) => {
207
+ const iw = imagePosition.bottomRight.x - imagePosition.topLeft.x
208
+ const ih = imagePosition.bottomRight.y - imagePosition.topLeft.y
209
+ if (isNaN(iw) || isNaN(ih)) return null
210
+ return (
211
+ <svg
212
+ width={iw}
213
+ height={ih}
214
+ style={{
215
+ position: "absolute",
216
+ zIndex: 2,
217
+ left: imagePosition.topLeft.x,
218
+ top: imagePosition.topLeft.y,
219
+ pointerEvents: "none",
220
+ width: iw,
221
+ height: ih,
222
+ }}
223
+ >
224
+ <WrappedRegionList
225
+ key="wrapped-region-list"
226
+ regions={regions}
227
+ iw={iw}
228
+ ih={ih}
229
+ keypointDefinitions={keypointDefinitions}
230
+ fullSegmentationMode={fullSegmentationMode}
231
+ />
232
+ </svg>
233
+ )
234
+ }
235
+
236
+ export default RegionShapes
@@ -0,0 +1,130 @@
1
+ // @flow weak
2
+
3
+ import React from "react"
4
+ import Paper from "@mui/material/Paper"
5
+ import DefaultRegionLabel from "../RegionLabel"
6
+ import LockIcon from "@mui/icons-material/Lock"
7
+
8
+ const copyWithout = (obj, ...args) => {
9
+ const newObj = { ...obj }
10
+ for (const arg of args) {
11
+ delete newObj[arg]
12
+ }
13
+ return newObj
14
+ }
15
+
16
+ export const RegionTags = ({
17
+ regions,
18
+ projectRegionBox,
19
+ mouseEvents,
20
+ regionClsList,
21
+ regionTagList,
22
+ onBeginRegionEdit,
23
+ onChangeRegion,
24
+ onCloseRegionEdit,
25
+ onDeleteRegion,
26
+ layoutParams,
27
+ imageSrc,
28
+ RegionEditLabel,
29
+ onRegionClassAdded,
30
+ allowComments,
31
+ }) => {
32
+ const RegionLabel =
33
+ RegionEditLabel != null ? RegionEditLabel : DefaultRegionLabel
34
+ return regions
35
+ .filter((r) => r.visible || r.visible === undefined)
36
+ .map((region) => {
37
+ const pbox = projectRegionBox(region)
38
+ const { iw, ih } = layoutParams.current
39
+ let margin = 8
40
+ if (region.highlighted && region.type === "box") margin += 6
41
+ const labelBoxHeight =
42
+ region.editingLabels && !region.locked ? 170 : region.tags ? 60 : 50
43
+ const displayOnTop = pbox.y > labelBoxHeight
44
+
45
+ const coords = displayOnTop
46
+ ? {
47
+ left: pbox.x,
48
+ top: pbox.y - margin / 2,
49
+ }
50
+ : { left: pbox.x, top: pbox.y + pbox.h + margin / 2 }
51
+ if (region.locked) {
52
+ return (
53
+ <div
54
+ key={region.id}
55
+ style={{
56
+ position: "absolute",
57
+ ...coords,
58
+ zIndex: 10 + (region.editingLabels ? 5 : 0),
59
+ }}
60
+ >
61
+ <Paper
62
+ style={{
63
+ position: "absolute",
64
+ left: 0,
65
+ ...(displayOnTop ? { bottom: 0 } : { top: 0 }),
66
+ zIndex: 10,
67
+ backgroundColor: "#fff",
68
+ borderRadius: 4,
69
+ padding: 2,
70
+ paddingBottom: 0,
71
+ opacity: 0.5,
72
+ pointerEvents: "none",
73
+ }}
74
+ >
75
+ <LockIcon style={{ width: 16, height: 16, color: "#333" }} />
76
+ </Paper>
77
+ </div>
78
+ )
79
+ }
80
+ return (
81
+ <div
82
+ key={region.id}
83
+ style={{
84
+ position: "absolute",
85
+ ...coords,
86
+ zIndex: 10 + (region.editingLabels ? 5 : 0),
87
+ width: 200,
88
+ }}
89
+ onMouseDown={(e) => e.preventDefault()}
90
+ onMouseUp={(e) => e.preventDefault()}
91
+ onMouseEnter={(e) => {
92
+ if (region.editingLabels) {
93
+ mouseEvents.onMouseUp(e)
94
+ e.button = 1
95
+ mouseEvents.onMouseUp(e)
96
+ }
97
+ }}
98
+ >
99
+ <div
100
+ style={{
101
+ position: "absolute",
102
+ zIndex: 20,
103
+ left: 0,
104
+ ...(displayOnTop ? { bottom: 0 } : { top: 0 }),
105
+ }}
106
+ {...(!region.editingLabels
107
+ ? copyWithout(mouseEvents, "onMouseDown", "onMouseUp")
108
+ : {})}
109
+ >
110
+ <RegionLabel
111
+ allowedClasses={regionClsList}
112
+ allowedTags={regionTagList}
113
+ onOpen={onBeginRegionEdit}
114
+ onChange={onChangeRegion}
115
+ onClose={onCloseRegionEdit}
116
+ onDelete={onDeleteRegion}
117
+ editing={region.editingLabels}
118
+ region={region}
119
+ regions={regions}
120
+ imageSrc={imageSrc}
121
+ onRegionClassAdded={onRegionClassAdded}
122
+ allowComments={allowComments}
123
+ />
124
+ </div>
125
+ </div>
126
+ )
127
+ })
128
+ }
129
+
130
+ export default RegionTags
@@ -0,0 +1,58 @@
1
+ // @flow
2
+
3
+ import React from "react"
4
+ import Dialog from "@mui/material/Dialog"
5
+ import DialogTitle from "@mui/material/DialogTitle"
6
+ import DialogContent from "@mui/material/DialogContent"
7
+ import DialogActions from "@mui/material/DialogActions"
8
+ import Button from "@mui/material/Button"
9
+ import Survey from "material-survey/components/Survey"
10
+ import { useSettings } from "../SettingsProvider"
11
+
12
+ export const SettingsDialog = ({ open, onClose }) => {
13
+ const settings = useSettings()
14
+ return (
15
+ <Dialog open={open || false} onClose={onClose}>
16
+ <DialogTitle>Settings</DialogTitle>
17
+ <DialogContent style={{ minWidth: 400 }}>
18
+ <Survey
19
+ variant="flat"
20
+ noActions
21
+ defaultAnswers={settings}
22
+ onQuestionChange={(q, a, answers) => settings.changeSetting(q, a)}
23
+ form={{
24
+ questions: [
25
+ {
26
+ type: "boolean",
27
+ title: "Show Crosshairs",
28
+ name: "showCrosshairs",
29
+ },
30
+ {
31
+ type: "boolean",
32
+ title: "Show Highlight Box",
33
+ name: "showHighlightBox",
34
+ },
35
+ {
36
+ type: "boolean",
37
+ title: "WASD Mode",
38
+ name: "wasdMode",
39
+ },
40
+ {
41
+ type: "dropdown",
42
+ title: "Video Playback Speed",
43
+ name: "videoPlaybackSpeed",
44
+ defaultValue: "1x",
45
+ choices: ["0.25x", "0.5x", "1x", "2x"],
46
+ },
47
+ ],
48
+ }}
49
+ />
50
+ </DialogContent>
51
+ <DialogActions>
52
+ <Button onClick={onClose}>Close</Button>
53
+ </DialogActions>
54
+ </Dialog>
55
+ )
56
+ }
57
+
58
+ export default SettingsDialog
@@ -0,0 +1,44 @@
1
+ // @flow
2
+
3
+ import React, { createContext, useContext, useState } from "react"
4
+
5
+ const defaultSettings = {
6
+ showCrosshairs: false,
7
+ showHighlightBox: true,
8
+ wasdMode: true,
9
+ }
10
+
11
+ export const SettingsContext = createContext(defaultSettings)
12
+
13
+ const pullSettingsFromLocalStorage = () => {
14
+ if (!window || !window.localStorage) return {}
15
+ let settings = {}
16
+ for (let i = 0; i < window.localStorage.length; i++) {
17
+ const key = window.localStorage.key(i)
18
+ if (key.startsWith("settings_")) {
19
+ try {
20
+ settings[key.replace("settings_", "")] = JSON.parse(
21
+ window.localStorage.getItem(key)
22
+ )
23
+ } catch (e) {}
24
+ }
25
+ }
26
+ return settings
27
+ }
28
+
29
+ export const useSettings = () => useContext(SettingsContext)
30
+
31
+ export const SettingsProvider = ({ children }) => {
32
+ const [state, changeState] = useState(() => pullSettingsFromLocalStorage())
33
+ const changeSetting = (setting: string, value: any) => {
34
+ changeState({ ...state, [setting]: value })
35
+ window.localStorage.setItem(`settings_${setting}`, JSON.stringify(value))
36
+ }
37
+ return (
38
+ <SettingsContext.Provider value={{ ...state, changeSetting }}>
39
+ {children}
40
+ </SettingsContext.Provider>
41
+ )
42
+ }
43
+
44
+ export default SettingsProvider
@@ -0,0 +1,44 @@
1
+ import React from "react"
2
+ import TextField from "@mui/material/TextField"
3
+ import { makeStyles } from "@mui/styles"
4
+ import { createTheme, ThemeProvider } from "@mui/material/styles"
5
+
6
+ const theme = createTheme()
7
+ const useStyles = makeStyles((theme) => ({
8
+ shortcutKeyFieldWrapper: {
9
+ paddingTop: 8,
10
+ display: "inline-flex",
11
+ width: "100%",
12
+ },
13
+ shortcutKeyText: {
14
+ lineHeight: 0,
15
+ },
16
+ shortcutTextfield: {
17
+ width: "100%",
18
+ boxSizing: "border-box",
19
+ textAlign: "center",
20
+ },
21
+ }))
22
+
23
+ const ShortcutField = ({ actionId, actionName, keyName, onChangeShortcut }) => {
24
+ const classes = useStyles()
25
+
26
+ return (
27
+ <ThemeProvider theme={theme}>
28
+ <div className={classes.shortcutKeyFieldWrapper}>
29
+ <TextField
30
+ variant="outlined"
31
+ label={actionName}
32
+ className={classes.shortcutTextfield}
33
+ value={keyName}
34
+ onKeyPress={(e) => {
35
+ onChangeShortcut(actionId, e.key)
36
+ e.stopPropagation()
37
+ }}
38
+ />
39
+ </div>
40
+ </ThemeProvider>
41
+ )
42
+ }
43
+
44
+ export default ShortcutField
@@ -0,0 +1,129 @@
1
+ import React, { useState, useEffect } from "react"
2
+ import SidebarBoxContainer from "../SidebarBoxContainer"
3
+ import { setIn } from "seamless-immutable"
4
+ import ShortcutField from "./ShortcutField"
5
+
6
+ const defaultShortcuts = {
7
+ select: {
8
+ action: {
9
+ type: "SELECT_TOOL",
10
+ },
11
+ name: "Select Region",
12
+ key: "Escape",
13
+ },
14
+ zoom: {
15
+ action: {
16
+ type: "SELECT_TOOL",
17
+ },
18
+ name: "Zoom In/Out",
19
+ key: "z",
20
+ },
21
+ "create-point": {
22
+ action: {
23
+ type: "SELECT_TOOL",
24
+ },
25
+ name: "Create Point",
26
+ },
27
+ "create-box": {
28
+ action: {
29
+ type: "SELECT_TOOL",
30
+ },
31
+ name: "Add Bounding Box",
32
+ key: "b",
33
+ },
34
+ pan: {
35
+ action: {
36
+ type: "SELECT_TOOL",
37
+ },
38
+ name: "Pan",
39
+ },
40
+ "create-polygon": {
41
+ action: {
42
+ type: "SELECT_TOOL",
43
+ },
44
+ name: "Create Polygon",
45
+ },
46
+ "create-pixel": {
47
+ action: {
48
+ type: "SELECT_TOOL",
49
+ },
50
+ name: "Create Pixel",
51
+ },
52
+ "prev-image": {
53
+ action: {
54
+ type: "HEADER_BUTTON_CLICKED",
55
+ buttonName: "Prev",
56
+ },
57
+ name: "Previous Image",
58
+ key: "a",
59
+ },
60
+ "next-image": {
61
+ action: {
62
+ type: "HEADER_BUTTON_CLICKED",
63
+ buttonName: "Next",
64
+ },
65
+ name: "Next Image",
66
+ key: "d", //"ArrowRight"
67
+ },
68
+ }
69
+
70
+ export default ({ onShortcutActionDispatched }) => {
71
+ const [shortcuts, setShortcuts] = useState({}) // useLocalStorage
72
+
73
+ useEffect(() => {
74
+ const newShortcuts = { ...shortcuts }
75
+ for (const actionId of Object.keys(defaultShortcuts)) {
76
+ if (!newShortcuts[actionId]) {
77
+ newShortcuts[actionId] = defaultShortcuts[actionId]
78
+ }
79
+ }
80
+ setShortcuts(newShortcuts)
81
+ }, [])
82
+
83
+ const onChangeShortcut = (actionId, keyName) => {
84
+ setShortcuts(setIn(shortcuts, [actionId, "key"], keyName))
85
+ }
86
+
87
+ useEffect(() => {
88
+ const handleKeyPress = (e) => {
89
+ for (const actionId in shortcuts) {
90
+ const shortcut = shortcuts[actionId]
91
+ if (!shortcut || !shortcut.key) {
92
+ continue
93
+ }
94
+ if (e.key === shortcut.key) {
95
+ onShortcutActionDispatched({
96
+ ...shortcut.action,
97
+ selectedTool: actionId,
98
+ })
99
+ }
100
+ }
101
+ }
102
+
103
+ window.addEventListener("keypress", handleKeyPress)
104
+
105
+ return () => {
106
+ window.removeEventListener("keypress", handleKeyPress)
107
+ document.activeElement.blur()
108
+ }
109
+ }, [shortcuts])
110
+
111
+ return (
112
+ <SidebarBoxContainer title="Shortcuts">
113
+ {Object.keys(shortcuts)
114
+ .map((actionId, index) => {
115
+ if (!shortcuts[actionId]) return null
116
+ return (
117
+ <ShortcutField
118
+ key={actionId}
119
+ actionId={actionId}
120
+ actionName={shortcuts[actionId].name}
121
+ keyName={shortcuts[actionId].key || ""}
122
+ onChangeShortcut={onChangeShortcut}
123
+ />
124
+ )
125
+ })
126
+ .filter(Boolean)}
127
+ </SidebarBoxContainer>
128
+ )
129
+ }