@umodeapporg/ui 0.24.4 → 0.25.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.
- package/dist/components/Form/ControlledTabs.js +1 -1
- package/dist/components/Form/ControlledViewToggle.js +1 -1
- package/dist/components/Navigation/UiNavigationContext.js +1 -1
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.d.ts +2 -1
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.js +138 -43
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.d.ts +3 -1
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js +3 -0
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.d.ts +16 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js +156 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.d.ts +8 -1
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js +30 -5
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.d.ts +12 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js +89 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.d.ts +3 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js +3 -2
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js +11 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.d.ts +12 -0
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js +64 -0
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.d.ts +4 -2
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js +23 -12
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js +2 -1
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.d.ts +10 -1
- package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +17 -2
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +30 -0
- package/dist/locales/pt/ui.json.d.ts +17 -2
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
|
|
|
6
6
|
import { useUiAccess } from "../UI/access/UiAccessContext.js";
|
|
7
7
|
import ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
8
8
|
import { formatNumber } from "../../helpers/formatHelpers.js";
|
|
9
|
-
import { claimUrlParam,
|
|
9
|
+
import { claimUrlParam, URL_PARAM_PRIORITY, TABS_URL_PARAM, releaseUrlParam, queueUrlParamUpdate } from "../../helpers/urlParamSync.js";
|
|
10
10
|
import LoadingSpinner from "../UI/LoadingSpinner.js";
|
|
11
11
|
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
12
12
|
import ControlledButton from "./ControlledButton.js";
|
|
@@ -8,7 +8,7 @@ import LoadingSpinner from "../UI/LoadingSpinner.js";
|
|
|
8
8
|
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
9
9
|
import { formatNumber } from "../../helpers/formatHelpers.js";
|
|
10
10
|
import { uiLogger } from "../../helpers/uiLogger.js";
|
|
11
|
-
import { TABS_URL_PARAM, claimUrlParam,
|
|
11
|
+
import { TABS_URL_PARAM, claimUrlParam, releaseUrlParam, ownsUrlParam, queueUrlParamUpdate, URL_PARAM_PRIORITY } from "../../helpers/urlParamSync.js";
|
|
12
12
|
const DEFAULT_URL_PARAM_KEY = "view";
|
|
13
13
|
const SIZE_STYLES = {
|
|
14
14
|
sm: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext, useMemo, createContext, forwardRef } from "react";
|
|
3
3
|
const FallbackLink = forwardRef(({ to, children, ...rest }, ref) => /* @__PURE__ */ jsx("a", { ref, href: to, ...rest, children }));
|
|
4
4
|
FallbackLink.displayName = "FallbackLink";
|
|
5
5
|
const createFallbackNavigation = () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
|
|
2
2
|
interface IAnnotationEditorProps {
|
|
3
3
|
baseImage: HTMLImageElement;
|
|
4
4
|
imageSize: IAnnotationSize;
|
|
5
5
|
mimeType: string;
|
|
6
6
|
imageName?: string;
|
|
7
7
|
initialElements: Array<TAnnotationElement>;
|
|
8
|
+
initialCrop: IAnnotationCropRect | null;
|
|
8
9
|
onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
|
|
9
10
|
onSaved: () => void;
|
|
10
11
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState,
|
|
2
|
+
import { useRef, useState, useMemo, useEffect, useCallback } from "react";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import { uiLogger } from "../../../helpers/uiLogger.js";
|
|
5
5
|
import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
|
|
6
6
|
import { useNotification } from "../../../hooks/useNotification.js";
|
|
7
7
|
import AnnotationStage from "./canvas/AnnotationStage.js";
|
|
8
8
|
import AnnotationActionBar from "./components/AnnotationActionBar.js";
|
|
9
|
+
import AnnotationCropBar from "./components/AnnotationCropBar.js";
|
|
9
10
|
import AnnotationElementActions from "./components/AnnotationElementActions.js";
|
|
10
11
|
import AnnotationStyleBar from "./components/AnnotationStyleBar.js";
|
|
11
12
|
import AnnotationTextOverlay from "./components/AnnotationTextOverlay.js";
|
|
12
13
|
import AnnotationToolbar from "./components/AnnotationToolbar.js";
|
|
13
14
|
import { fitStageToViewport } from "./helpers/annotationCanvas.js";
|
|
14
|
-
import {
|
|
15
|
+
import { fullImageCrop, aspectRatioFor, fitCropToAspect, isFullImageCrop } from "./helpers/annotationCrop.js";
|
|
16
|
+
import { styleFromElement, applyStyleToElement, translateElement, createImageElement, createTextElement, imageMaxDimension } from "./helpers/annotationElements.js";
|
|
15
17
|
import { exportStageToFile } from "./helpers/annotationExport.js";
|
|
16
18
|
import { useAnnotationDocument } from "./hooks/useAnnotationDocument.js";
|
|
17
19
|
import { useAnnotationShortcuts } from "./hooks/useAnnotationShortcuts.js";
|
|
18
20
|
import { useCoarsePointer } from "./hooks/useCoarsePointer.js";
|
|
19
21
|
import { DEFAULT_ANNOTATION_STYLE } from "./annotationStyle.js";
|
|
22
|
+
const CROP_STAGE_GUTTER_PX = 12;
|
|
20
23
|
const ELEMENT_TOOLS = ["freehand", "line", "arrow", "rectangle", "ellipse", "text", "image"];
|
|
21
24
|
const readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
22
25
|
const reader = new FileReader();
|
|
@@ -37,6 +40,7 @@ const AnnotationEditor = ({
|
|
|
37
40
|
mimeType,
|
|
38
41
|
imageName,
|
|
39
42
|
initialElements,
|
|
43
|
+
initialCrop,
|
|
40
44
|
onSave,
|
|
41
45
|
onSaved
|
|
42
46
|
}) => {
|
|
@@ -52,8 +56,19 @@ const AnnotationEditor = ({
|
|
|
52
56
|
const [style, setStyle] = useState(DEFAULT_ANNOTATION_STYLE);
|
|
53
57
|
const [editingText, setEditingText] = useState(null);
|
|
54
58
|
const [saving, setSaving] = useState(false);
|
|
55
|
-
const
|
|
59
|
+
const [cropDraft, setCropDraft] = useState(null);
|
|
60
|
+
const [cropAspect, setCropAspect] = useState("free");
|
|
61
|
+
const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);
|
|
56
62
|
const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;
|
|
63
|
+
const isCropping = tool === "crop";
|
|
64
|
+
const viewRect = useMemo(
|
|
65
|
+
() => isCropping ? fullImageCrop(imageSize) : controller.crop ?? fullImageCrop(imageSize),
|
|
66
|
+
[isCropping, controller.crop, imageSize]
|
|
67
|
+
);
|
|
68
|
+
const viewSize = useMemo(
|
|
69
|
+
() => ({ width: viewRect.width, height: viewRect.height }),
|
|
70
|
+
[viewRect.width, viewRect.height]
|
|
71
|
+
);
|
|
57
72
|
useEffect(() => {
|
|
58
73
|
const node = viewportRef.current;
|
|
59
74
|
if (!node) return;
|
|
@@ -64,7 +79,14 @@ const AnnotationEditor = ({
|
|
|
64
79
|
observer.observe(node);
|
|
65
80
|
return () => observer.disconnect();
|
|
66
81
|
}, []);
|
|
67
|
-
const
|
|
82
|
+
const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;
|
|
83
|
+
const fit = useMemo(
|
|
84
|
+
() => fitStageToViewport(viewSize, {
|
|
85
|
+
width: viewport.width - stageGutter * 2,
|
|
86
|
+
height: viewport.height - stageGutter * 2
|
|
87
|
+
}),
|
|
88
|
+
[viewSize, viewport, stageGutter]
|
|
89
|
+
);
|
|
68
90
|
useEffect(() => {
|
|
69
91
|
if (!selectedId) return;
|
|
70
92
|
setStyle((current) => {
|
|
@@ -99,23 +121,72 @@ const AnnotationEditor = ({
|
|
|
99
121
|
const handleToolChange = useCallback(
|
|
100
122
|
(next) => {
|
|
101
123
|
handleTextCommit();
|
|
124
|
+
if (next === "crop") {
|
|
125
|
+
controller.select(null);
|
|
126
|
+
setCropDraft(controller.crop ?? fullImageCrop(imageSize));
|
|
127
|
+
setCropAspect("free");
|
|
128
|
+
}
|
|
102
129
|
setTool(next);
|
|
103
130
|
},
|
|
104
|
-
[handleTextCommit]
|
|
131
|
+
[handleTextCommit, controller, imageSize]
|
|
105
132
|
);
|
|
133
|
+
const handleCropAspectChange = useCallback(
|
|
134
|
+
(aspect) => {
|
|
135
|
+
setCropAspect(aspect);
|
|
136
|
+
const ratio = aspectRatioFor(aspect, imageSize);
|
|
137
|
+
if (ratio === null) return;
|
|
138
|
+
setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));
|
|
139
|
+
},
|
|
140
|
+
[imageSize]
|
|
141
|
+
);
|
|
142
|
+
const handleCropReset = useCallback(() => {
|
|
143
|
+
setCropAspect("free");
|
|
144
|
+
setCropDraft(fullImageCrop(imageSize));
|
|
145
|
+
}, [imageSize]);
|
|
146
|
+
const handleCropCancel = useCallback(() => {
|
|
147
|
+
setCropDraft(null);
|
|
148
|
+
setTool("select");
|
|
149
|
+
}, []);
|
|
150
|
+
const handleCropApply = useCallback(() => {
|
|
151
|
+
const draft = cropDraft;
|
|
152
|
+
setCropDraft(null);
|
|
153
|
+
setTool("select");
|
|
154
|
+
if (!draft) return;
|
|
155
|
+
const next = isFullImageCrop(draft, imageSize) ? null : draft;
|
|
156
|
+
const current = controller.crop;
|
|
157
|
+
const unchanged = next === current || next !== null && current !== null && next.x === current.x && next.y === current.y && next.width === current.width && next.height === current.height;
|
|
158
|
+
if (!unchanged) controller.setCrop(next);
|
|
159
|
+
}, [cropDraft, imageSize, controller]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!isCropping) return;
|
|
162
|
+
const handleKeyDown = (event) => {
|
|
163
|
+
if (event.key === "Escape") {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
handleCropCancel();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (event.key === "Enter") {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
handleCropApply();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
175
|
+
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
|
176
|
+
}, [isCropping, handleCropCancel, handleCropApply]);
|
|
106
177
|
const handleInsertImage = useCallback(
|
|
107
178
|
async (file) => {
|
|
108
179
|
try {
|
|
109
180
|
const source = await readFileAsDataUrl(file);
|
|
110
181
|
const natural = await loadNaturalSize(source);
|
|
111
|
-
addElement(createImageElement(source, natural,
|
|
182
|
+
addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));
|
|
112
183
|
setTool("select");
|
|
113
184
|
} catch (error) {
|
|
114
185
|
uiLogger.error("Failed to insert an image into the annotation", error);
|
|
115
186
|
notify({ message: t("imageAnnotator.imageInsertFailed"), type: "error" });
|
|
116
187
|
}
|
|
117
188
|
},
|
|
118
|
-
[addElement,
|
|
189
|
+
[addElement, viewSize, viewRect.x, viewRect.y, notify, t]
|
|
119
190
|
);
|
|
120
191
|
const handleRequestTextEdit = useCallback(
|
|
121
192
|
(id) => {
|
|
@@ -144,14 +215,14 @@ const AnnotationEditor = ({
|
|
|
144
215
|
}, [editingText, controller.elements, removeElement]);
|
|
145
216
|
const handleSave = async () => {
|
|
146
217
|
const stage = stageRef.current;
|
|
147
|
-
if (!stage || saving) return;
|
|
218
|
+
if (!stage || saving || isCropping) return;
|
|
148
219
|
setSaving(true);
|
|
149
220
|
handleTextCommit();
|
|
150
221
|
controller.select(null);
|
|
151
222
|
try {
|
|
152
223
|
await nextFrame();
|
|
153
224
|
await nextFrame();
|
|
154
|
-
const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });
|
|
225
|
+
const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });
|
|
155
226
|
await onSave(file, controller.document);
|
|
156
227
|
onSaved();
|
|
157
228
|
} catch (error) {
|
|
@@ -162,7 +233,7 @@ const AnnotationEditor = ({
|
|
|
162
233
|
}
|
|
163
234
|
};
|
|
164
235
|
useAnnotationShortcuts({
|
|
165
|
-
active: !editingText,
|
|
236
|
+
active: !editingText && !isCropping,
|
|
166
237
|
controller,
|
|
167
238
|
imageMaxDimension: imageMaxDimension(imageSize),
|
|
168
239
|
onToolChange: handleToolChange
|
|
@@ -189,10 +260,10 @@ const AnnotationEditor = ({
|
|
|
189
260
|
{
|
|
190
261
|
touch,
|
|
191
262
|
saving,
|
|
192
|
-
canUndo: controller.canUndo,
|
|
193
|
-
canRedo: controller.canRedo,
|
|
194
|
-
canClear:
|
|
195
|
-
canSave: !controller.isEmpty,
|
|
263
|
+
canUndo: controller.canUndo && !isCropping,
|
|
264
|
+
canRedo: controller.canRedo && !isCropping,
|
|
265
|
+
canClear: controller.elements.length > 0 && !isCropping,
|
|
266
|
+
canSave: !controller.isEmpty && !isCropping,
|
|
196
267
|
elementActions: touch ? elementActions : void 0,
|
|
197
268
|
onUndo: controller.undo,
|
|
198
269
|
onRedo: controller.redo,
|
|
@@ -211,7 +282,18 @@ const AnnotationEditor = ({
|
|
|
211
282
|
onInsertImage: handleInsertImage
|
|
212
283
|
}
|
|
213
284
|
),
|
|
214
|
-
/* @__PURE__ */ jsx(
|
|
285
|
+
isCropping ? /* @__PURE__ */ jsx(
|
|
286
|
+
AnnotationCropBar,
|
|
287
|
+
{
|
|
288
|
+
aspect: cropAspect,
|
|
289
|
+
touch,
|
|
290
|
+
canReset: !isFullImageCrop(cropDraft, imageSize),
|
|
291
|
+
onAspectChange: handleCropAspectChange,
|
|
292
|
+
onReset: handleCropReset,
|
|
293
|
+
onCancel: handleCropCancel,
|
|
294
|
+
onApply: handleCropApply
|
|
295
|
+
}
|
|
296
|
+
) : /* @__PURE__ */ jsx(
|
|
215
297
|
AnnotationStyleBar,
|
|
216
298
|
{
|
|
217
299
|
style,
|
|
@@ -228,34 +310,47 @@ const AnnotationEditor = ({
|
|
|
228
310
|
{
|
|
229
311
|
ref: viewportRef,
|
|
230
312
|
className: "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3",
|
|
231
|
-
children: fit.width > 1 && /* @__PURE__ */ jsxs(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
children: fit.width > 1 && /* @__PURE__ */ jsxs(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
className: "relative",
|
|
317
|
+
style: { width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 },
|
|
318
|
+
children: [
|
|
319
|
+
/* @__PURE__ */ jsx(
|
|
320
|
+
AnnotationStage,
|
|
321
|
+
{
|
|
322
|
+
baseImage,
|
|
323
|
+
imageSize,
|
|
324
|
+
fit,
|
|
325
|
+
viewRect,
|
|
326
|
+
cropDraft,
|
|
327
|
+
cropAspectRatio: aspectRatioFor(cropAspect, imageSize),
|
|
328
|
+
gutter: stageGutter,
|
|
329
|
+
onCropDraftChange: setCropDraft,
|
|
330
|
+
tool,
|
|
331
|
+
style,
|
|
332
|
+
controller,
|
|
333
|
+
editingTextId: editingText?.id ?? null,
|
|
334
|
+
stageRef,
|
|
335
|
+
onRequestTextEdit: handleRequestTextEdit,
|
|
336
|
+
onTextPlaced: handleTextPlaced
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
editingText && editingElement && /* @__PURE__ */ jsx(
|
|
340
|
+
AnnotationTextOverlay,
|
|
341
|
+
{
|
|
342
|
+
element: editingElement,
|
|
343
|
+
viewScale: fit.scale,
|
|
344
|
+
viewOrigin: viewRect,
|
|
345
|
+
value: editingText.value,
|
|
346
|
+
onChange: (value) => setEditingText((current) => current ? { ...current, value } : current),
|
|
347
|
+
onCommit: handleTextCommit,
|
|
348
|
+
onCancel: handleTextCancel
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
)
|
|
259
354
|
}
|
|
260
355
|
),
|
|
261
356
|
touch && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: actionBar })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n\n const controller = useAnnotationDocument(imageSize, initialElements);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const fit = useMemo(() => fitStageToViewport(imageSize, viewport), [imageSize, viewport]);\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n setTool(next);\n },\n [handleTextCommit],\n );\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(createImageElement(source, natural, imageSize));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, imageSize, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo}\n canRedo={controller.canRedo}\n canClear={!controller.isEmpty}\n canSave={!controller.isEmpty}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div className=\"relative\" style={{ width: fit.width, height: fit.height }}>\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationCropBar from './components/AnnotationCropBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n aspectRatioFor,\n fitCropToAspect,\n fullImageCrop,\n isFullImageCrop,\n TAnnotationCropAspect,\n} from './helpers/annotationCrop';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n translateElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationCropRect,\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst CROP_STAGE_GUTTER_PX = 12;\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n initialCrop: IAnnotationCropRect | null;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n initialCrop,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n const [cropDraft, setCropDraft] = useState<IAnnotationCropRect | null>(null);\n const [cropAspect, setCropAspect] = useState<TAnnotationCropAspect>('free');\n\n const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n const isCropping = tool === 'crop';\n const viewRect = useMemo<IAnnotationCropRect>(\n () => (isCropping ? fullImageCrop(imageSize) : (controller.crop ?? fullImageCrop(imageSize))),\n [isCropping, controller.crop, imageSize],\n );\n const viewSize = useMemo<IAnnotationSize>(\n () => ({ width: viewRect.width, height: viewRect.height }),\n [viewRect.width, viewRect.height],\n );\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;\n const fit = useMemo(\n () =>\n fitStageToViewport(viewSize, {\n width: viewport.width - stageGutter * 2,\n height: viewport.height - stageGutter * 2,\n }),\n [viewSize, viewport, stageGutter],\n );\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n if (next === 'crop') {\n controller.select(null);\n setCropDraft(controller.crop ?? fullImageCrop(imageSize));\n setCropAspect('free');\n }\n setTool(next);\n },\n [handleTextCommit, controller, imageSize],\n );\n\n const handleCropAspectChange = useCallback(\n (aspect: TAnnotationCropAspect) => {\n setCropAspect(aspect);\n const ratio = aspectRatioFor(aspect, imageSize);\n if (ratio === null) return;\n setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));\n },\n [imageSize],\n );\n\n const handleCropReset = useCallback(() => {\n setCropAspect('free');\n setCropDraft(fullImageCrop(imageSize));\n }, [imageSize]);\n\n const handleCropCancel = useCallback(() => {\n setCropDraft(null);\n setTool('select');\n }, []);\n\n const handleCropApply = useCallback(() => {\n const draft = cropDraft;\n setCropDraft(null);\n setTool('select');\n if (!draft) return;\n const next = isFullImageCrop(draft, imageSize) ? null : draft;\n const current = controller.crop;\n const unchanged =\n next === current ||\n (next !== null &&\n current !== null &&\n next.x === current.x &&\n next.y === current.y &&\n next.width === current.width &&\n next.height === current.height);\n if (!unchanged) controller.setCrop(next);\n }, [cropDraft, imageSize, controller]);\n\n useEffect(() => {\n if (!isCropping) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n handleCropCancel();\n return;\n }\n if (event.key === 'Enter') {\n event.preventDefault();\n handleCropApply();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown, true);\n return () => window.removeEventListener('keydown', handleKeyDown, true);\n }, [isCropping, handleCropCancel, handleCropApply]);\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, viewSize, viewRect.x, viewRect.y, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving || isCropping) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText && !isCropping,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo && !isCropping}\n canRedo={controller.canRedo && !isCropping}\n canClear={controller.elements.length > 0 && !isCropping}\n canSave={!controller.isEmpty && !isCropping}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n {isCropping ? (\n <AnnotationCropBar\n aspect={cropAspect}\n touch={touch}\n canReset={!isFullImageCrop(cropDraft, imageSize)}\n onAspectChange={handleCropAspectChange}\n onReset={handleCropReset}\n onCancel={handleCropCancel}\n onApply={handleCropApply}\n />\n ) : (\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n )}\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div\n className=\"relative\"\n style={{ width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 }}\n >\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n viewRect={viewRect}\n cropDraft={cropDraft}\n cropAspectRatio={aspectRatioFor(cropAspect, imageSize)}\n gutter={stageGutter}\n onCropDraftChange={setCropDraft}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n viewOrigin={viewRect}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
|
|
2
2
|
interface IImageAnnotationDialogProps {
|
|
3
3
|
open: boolean;
|
|
4
4
|
imageUrl: string;
|
|
5
5
|
imageName?: string;
|
|
6
6
|
/** Previously saved annotations, when the host persists the document alongside the file. */
|
|
7
7
|
initialElements?: Array<TAnnotationElement>;
|
|
8
|
+
/** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */
|
|
9
|
+
initialCrop?: IAnnotationCropRect | null;
|
|
8
10
|
onClose: () => void;
|
|
9
11
|
/** Receives the flattened image plus the editable document; resolving closes the dialog. */
|
|
10
12
|
onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
|
|
@@ -5,6 +5,7 @@ import ControlledDialog from "../../Form/ControlledDialog.js";
|
|
|
5
5
|
import LoadingSpinner from "../LoadingSpinner.js";
|
|
6
6
|
import NoDataFound from "../NoDataFound/index.js";
|
|
7
7
|
import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
|
|
8
|
+
import { sanitizeInitialCrop } from "./helpers/annotationCrop.js";
|
|
8
9
|
import { useAnnotationBaseImage } from "./hooks/useAnnotationBaseImage.js";
|
|
9
10
|
const AnnotationEditor = lazy(() => import("./AnnotationEditor.js"));
|
|
10
11
|
const ImageAnnotationDialog = ({
|
|
@@ -12,6 +13,7 @@ const ImageAnnotationDialog = ({
|
|
|
12
13
|
imageUrl,
|
|
13
14
|
imageName,
|
|
14
15
|
initialElements,
|
|
16
|
+
initialCrop,
|
|
15
17
|
onClose,
|
|
16
18
|
onSave
|
|
17
19
|
}) => {
|
|
@@ -42,6 +44,7 @@ const ImageAnnotationDialog = ({
|
|
|
42
44
|
mimeType,
|
|
43
45
|
imageName,
|
|
44
46
|
initialElements: initialElements ?? [],
|
|
47
|
+
initialCrop: sanitizeInitialCrop(initialCrop, size),
|
|
45
48
|
onSave,
|
|
46
49
|
onSaved: onClose
|
|
47
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { sanitizeInitialCrop } from './helpers/annotationCrop';\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n /** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */\n initialCrop?: IAnnotationCropRect | null;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n initialCrop,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n initialCrop={sanitizeInitialCrop(initialCrop, size)}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":";;;;;;;;;AAaA,MAAM,mBAAmB,KAAK,MAAM,OAAO,uBAAoB,CAAC;AAehE,MAAM,wBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,SAAA,IAAa,cAAA;AACrB,QAAM,EAAE,OAAO,MAAM,UAAU,SAAS,WAAW,uBAAuB,MAAM,QAAQ;AAExF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAM,WAAW,QAAQ;AAAA,MACzB,OAAO,EAAE,sBAAsB;AAAA,MAC/B,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAO;AAAA,MAEP,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACX,UAAA;AAAA,SAAA,WAAY,CAAC,SAAS,CAAC,WACvB,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,GAC3B;AAAA,QAGD,UACC,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,aAAA,EAAY,SAAQ,SAAQ,MAAK,MAAK,OAAO,EAAE,0BAA0B,GAAG,GAC/E;AAAA,QAGD,SAAS,QACR;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,8BACG,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,EAAA,CAC3B;AAAA,YAGF,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAW;AAAA,gBACX,WAAW;AAAA,gBACX;AAAA,gBACA;AAAA,gBACA,iBAAiB,mBAAmB,CAAA;AAAA,gBACpC,aAAa,oBAAoB,aAAa,IAAI;AAAA,gBAClD;AAAA,gBACA,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX;AAAA,QAAA;AAAA,MACF,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationSize } from '../interfaces/annotationDocument';
|
|
2
|
+
interface IAnnotationCropFrameProps {
|
|
3
|
+
crop: IAnnotationCropRect;
|
|
4
|
+
imageSize: IAnnotationSize;
|
|
5
|
+
viewScale: number;
|
|
6
|
+
/** Screen-pixel offset of the image inside the stage; absolute bounds start here. */
|
|
7
|
+
gutter: number;
|
|
8
|
+
aspectRatio: number | null;
|
|
9
|
+
onChange: (crop: IAnnotationCropRect) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Drawn on a stage that shows the whole image, so the frame's local coordinates are image pixels
|
|
13
|
+
* and its absolute coordinates are those pixels times the view scale, shifted by the gutter.
|
|
14
|
+
*/
|
|
15
|
+
declare const AnnotationCropFrame: React.FC<IAnnotationCropFrameProps>;
|
|
16
|
+
export default AnnotationCropFrame;
|