@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.
Files changed (39) hide show
  1. package/dist/components/Form/ControlledTabs.js +1 -1
  2. package/dist/components/Form/ControlledViewToggle.js +1 -1
  3. package/dist/components/Navigation/UiNavigationContext.js +1 -1
  4. package/dist/components/UI/ImageAnnotator/AnnotationEditor.d.ts +2 -1
  5. package/dist/components/UI/ImageAnnotator/AnnotationEditor.js +138 -43
  6. package/dist/components/UI/ImageAnnotator/AnnotationEditor.js.map +1 -1
  7. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.d.ts +3 -1
  8. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js +3 -0
  9. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js.map +1 -1
  10. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.d.ts +16 -0
  11. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js +156 -0
  12. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js.map +1 -0
  13. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.d.ts +8 -1
  14. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js +30 -5
  15. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js.map +1 -1
  16. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.d.ts +12 -0
  17. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js +89 -0
  18. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js.map +1 -0
  19. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.d.ts +3 -1
  20. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js +3 -2
  21. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js.map +1 -1
  22. package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js +11 -0
  23. package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js.map +1 -1
  24. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.d.ts +12 -0
  25. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js +64 -0
  26. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js.map +1 -0
  27. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.d.ts +4 -2
  28. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js +23 -12
  29. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js.map +1 -1
  30. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js +2 -1
  31. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js.map +1 -1
  32. package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.d.ts +10 -1
  33. package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.js.map +1 -1
  34. package/dist/locales/en/ui.json.d.ts +17 -2
  35. package/dist/locales/en/ui.json.js +1 -1
  36. package/dist/locales/index.d.ts +30 -0
  37. package/dist/locales/pt/ui.json.d.ts +17 -2
  38. package/dist/locales/pt/ui.json.js +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ const ANNOTATION_CROP_ASPECTS = [
2
+ "free",
3
+ "original",
4
+ "square",
5
+ "portrait",
6
+ "classic",
7
+ "wide"
8
+ ];
9
+ const FIXED_ASPECT_RATIOS = {
10
+ square: 1,
11
+ portrait: 4 / 5,
12
+ classic: 3 / 4,
13
+ wide: 16 / 9
14
+ };
15
+ const MIN_CROP_FRACTION = 0.05;
16
+ const fullImageCrop = (size) => ({
17
+ x: 0,
18
+ y: 0,
19
+ width: size.width,
20
+ height: size.height
21
+ });
22
+ const minCropDimension = (size) => Math.max(16, Math.round(Math.min(size.width, size.height) * MIN_CROP_FRACTION));
23
+ const aspectRatioFor = (aspect, size) => {
24
+ if (aspect === "free") return null;
25
+ if (aspect === "original") return size.width / size.height;
26
+ return FIXED_ASPECT_RATIOS[aspect];
27
+ };
28
+ const clampCropToImage = (crop, size) => {
29
+ const minimum = minCropDimension(size);
30
+ const width = Math.min(Math.max(crop.width, minimum), size.width);
31
+ const height = Math.min(Math.max(crop.height, minimum), size.height);
32
+ return {
33
+ x: Math.round(Math.min(Math.max(crop.x, 0), size.width - width)),
34
+ y: Math.round(Math.min(Math.max(crop.y, 0), size.height - height)),
35
+ width: Math.round(width),
36
+ height: Math.round(height)
37
+ };
38
+ };
39
+ const fitCropToAspect = (crop, ratio, size) => {
40
+ const width = Math.min(size.width, size.height * ratio);
41
+ const height = width / ratio;
42
+ const centerX = crop.x + crop.width / 2;
43
+ const centerY = crop.y + crop.height / 2;
44
+ return clampCropToImage({ x: centerX - width / 2, y: centerY - height / 2, width, height }, size);
45
+ };
46
+ const isFullImageCrop = (crop, size) => !crop || crop.x <= 0 && crop.y <= 0 && crop.width >= size.width && crop.height >= size.height;
47
+ const sanitizeInitialCrop = (crop, size) => {
48
+ if (!crop) return null;
49
+ const values = [crop.x, crop.y, crop.width, crop.height];
50
+ if (values.some((value) => typeof value !== "number" || !Number.isFinite(value))) return null;
51
+ const clamped = clampCropToImage(crop, size);
52
+ return isFullImageCrop(clamped, size) ? null : clamped;
53
+ };
54
+ export {
55
+ ANNOTATION_CROP_ASPECTS,
56
+ aspectRatioFor,
57
+ clampCropToImage,
58
+ fitCropToAspect,
59
+ fullImageCrop,
60
+ isFullImageCrop,
61
+ minCropDimension,
62
+ sanitizeInitialCrop
63
+ };
64
+ //# sourceMappingURL=annotationCrop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotationCrop.js","sources":["../../../../../src/components/UI/ImageAnnotator/helpers/annotationCrop.ts"],"sourcesContent":["import { IAnnotationCropRect, IAnnotationSize } from '../interfaces/annotationDocument';\n\nexport type TAnnotationCropAspect = 'free' | 'original' | 'square' | 'portrait' | 'classic' | 'wide';\n\nexport const ANNOTATION_CROP_ASPECTS: Array<TAnnotationCropAspect> = [\n 'free',\n 'original',\n 'square',\n 'portrait',\n 'classic',\n 'wide',\n];\n\nconst FIXED_ASPECT_RATIOS: Record<Exclude<TAnnotationCropAspect, 'free' | 'original'>, number> = {\n square: 1,\n portrait: 4 / 5,\n classic: 3 / 4,\n wide: 16 / 9,\n};\n\n/** Below this the crop handles overlap and the result is a smudge nobody meant to keep. */\nconst MIN_CROP_FRACTION = 0.05;\n\nexport const fullImageCrop = (size: IAnnotationSize): IAnnotationCropRect => ({\n x: 0,\n y: 0,\n width: size.width,\n height: size.height,\n});\n\nexport const minCropDimension = (size: IAnnotationSize): number =>\n Math.max(16, Math.round(Math.min(size.width, size.height) * MIN_CROP_FRACTION));\n\nexport const aspectRatioFor = (aspect: TAnnotationCropAspect, size: IAnnotationSize): number | null => {\n if (aspect === 'free') return null;\n if (aspect === 'original') return size.width / size.height;\n return FIXED_ASPECT_RATIOS[aspect];\n};\n\nexport const clampCropToImage = (crop: IAnnotationCropRect, size: IAnnotationSize): IAnnotationCropRect => {\n const minimum = minCropDimension(size);\n const width = Math.min(Math.max(crop.width, minimum), size.width);\n const height = Math.min(Math.max(crop.height, minimum), size.height);\n\n return {\n x: Math.round(Math.min(Math.max(crop.x, 0), size.width - width)),\n y: Math.round(Math.min(Math.max(crop.y, 0), size.height - height)),\n width: Math.round(width),\n height: Math.round(height),\n };\n};\n\n/** Largest rect with the requested ratio, kept on the current crop's center so the framing does not jump. */\nexport const fitCropToAspect = (\n crop: IAnnotationCropRect,\n ratio: number,\n size: IAnnotationSize,\n): IAnnotationCropRect => {\n const width = Math.min(size.width, size.height * ratio);\n const height = width / ratio;\n const centerX = crop.x + crop.width / 2;\n const centerY = crop.y + crop.height / 2;\n\n return clampCropToImage({ x: centerX - width / 2, y: centerY - height / 2, width, height }, size);\n};\n\nexport const isFullImageCrop = (crop: IAnnotationCropRect | null, size: IAnnotationSize): boolean =>\n !crop || (crop.x <= 0 && crop.y <= 0 && crop.width >= size.width && crop.height >= size.height);\n\n/** A saved document is untrusted input: a crop that does not fit the image is dropped, never applied. */\nexport const sanitizeInitialCrop = (\n crop: IAnnotationCropRect | null | undefined,\n size: IAnnotationSize,\n): IAnnotationCropRect | null => {\n if (!crop) return null;\n const values = [crop.x, crop.y, crop.width, crop.height];\n if (values.some((value) => typeof value !== 'number' || !Number.isFinite(value))) return null;\n\n const clamped = clampCropToImage(crop, size);\n return isFullImageCrop(clamped, size) ? null : clamped;\n};\n"],"names":[],"mappings":"AAIO,MAAM,0BAAwD;AAAA,EACnE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,sBAA2F;AAAA,EAC/F,QAAQ;AAAA,EACR,UAAU,IAAI;AAAA,EACd,SAAS,IAAI;AAAA,EACb,MAAM,KAAK;AACb;AAGA,MAAM,oBAAoB;AAEnB,MAAM,gBAAgB,CAAC,UAAgD;AAAA,EAC5E,GAAG;AAAA,EACH,GAAG;AAAA,EACH,OAAO,KAAK;AAAA,EACZ,QAAQ,KAAK;AACf;AAEO,MAAM,mBAAmB,CAAC,SAC/B,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,MAAM,IAAI,iBAAiB,CAAC;AAEzE,MAAM,iBAAiB,CAAC,QAA+B,SAAyC;AACrG,MAAI,WAAW,OAAQ,QAAO;AAC9B,MAAI,WAAW,WAAY,QAAO,KAAK,QAAQ,KAAK;AACpD,SAAO,oBAAoB,MAAM;AACnC;AAEO,MAAM,mBAAmB,CAAC,MAA2B,SAA+C;AACzG,QAAM,UAAU,iBAAiB,IAAI;AACrC,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,OAAO,OAAO,GAAG,KAAK,KAAK;AAChE,QAAM,SAAS,KAAK,IAAI,KAAK,IAAI,KAAK,QAAQ,OAAO,GAAG,KAAK,MAAM;AAEnE,SAAO;AAAA,IACL,GAAG,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;AAAA,IAC/D,GAAG,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC,GAAG,KAAK,SAAS,MAAM,CAAC;AAAA,IACjE,OAAO,KAAK,MAAM,KAAK;AAAA,IACvB,QAAQ,KAAK,MAAM,MAAM;AAAA,EAAA;AAE7B;AAGO,MAAM,kBAAkB,CAC7B,MACA,OACA,SACwB;AACxB,QAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,KAAK;AACtD,QAAM,SAAS,QAAQ;AACvB,QAAM,UAAU,KAAK,IAAI,KAAK,QAAQ;AACtC,QAAM,UAAU,KAAK,IAAI,KAAK,SAAS;AAEvC,SAAO,iBAAiB,EAAE,GAAG,UAAU,QAAQ,GAAG,GAAG,UAAU,SAAS,GAAG,OAAO,OAAA,GAAU,IAAI;AAClG;AAEO,MAAM,kBAAkB,CAAC,MAAkC,SAChE,CAAC,QAAS,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,SAAS,KAAK,SAAS,KAAK,UAAU,KAAK;AAGnF,MAAM,sBAAsB,CACjC,MACA,SAC+B;AAC/B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,OAAO,KAAK,MAAM;AACvD,MAAI,OAAO,KAAK,CAAC,UAAU,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,CAAC,EAAG,QAAO;AAEzF,QAAM,UAAU,iBAAiB,MAAM,IAAI;AAC3C,SAAO,gBAAgB,SAAS,IAAI,IAAI,OAAO;AACjD;"}
@@ -1,13 +1,15 @@
1
- import { IAnnotationDocument, IAnnotationSize, TAnnotationElement } from '../interfaces/annotationDocument';
1
+ import { IAnnotationCropRect, IAnnotationDocument, IAnnotationSize, TAnnotationElement } from '../interfaces/annotationDocument';
2
2
  type TElementList = Array<TAnnotationElement>;
3
3
  export interface IAnnotationDocumentController {
4
4
  elements: TElementList;
5
+ crop: IAnnotationCropRect | null;
5
6
  document: IAnnotationDocument;
6
7
  selectedId: string | null;
7
8
  selectedElement: TAnnotationElement | null;
8
9
  canUndo: boolean;
9
10
  canRedo: boolean;
10
11
  isEmpty: boolean;
12
+ setCrop: (crop: IAnnotationCropRect | null) => void;
11
13
  select: (id: string | null) => void;
12
14
  addElement: (element: TAnnotationElement, options?: {
13
15
  select?: boolean;
@@ -23,5 +25,5 @@ export interface IAnnotationDocumentController {
23
25
  redo: () => void;
24
26
  clear: () => void;
25
27
  }
26
- export declare const useAnnotationDocument: (size: IAnnotationSize, initialElements?: TElementList) => IAnnotationDocumentController;
28
+ export declare const useAnnotationDocument: (size: IAnnotationSize, initialElements?: TElementList, initialCrop?: IAnnotationCropRect | null) => IAnnotationDocumentController;
27
29
  export {};
@@ -5,10 +5,12 @@ const HISTORY_LIMIT = 50;
5
5
  const historyReducer = (state, action) => {
6
6
  switch (action.type) {
7
7
  case "commit":
8
- if (action.elements === state.present) return state;
8
+ if (action.snapshot.elements === state.present.elements && action.snapshot.crop === state.present.crop) {
9
+ return state;
10
+ }
9
11
  return {
10
12
  past: [...state.past, state.present].slice(-HISTORY_LIMIT),
11
- present: action.elements,
13
+ present: action.snapshot,
12
14
  future: []
13
15
  };
14
16
  case "undo": {
@@ -30,26 +32,33 @@ const historyReducer = (state, action) => {
30
32
  };
31
33
  }
32
34
  case "reset":
33
- return { past: [], present: action.elements, future: [] };
35
+ return { past: [], present: action.snapshot, future: [] };
34
36
  default:
35
37
  return state;
36
38
  }
37
39
  };
38
- const useAnnotationDocument = (size, initialElements = []) => {
40
+ const useAnnotationDocument = (size, initialElements = [], initialCrop = null) => {
39
41
  const [history, dispatch] = useReducer(historyReducer, {
40
42
  past: [],
41
- present: initialElements,
43
+ present: { elements: initialElements, crop: initialCrop },
42
44
  future: []
43
45
  });
44
46
  const [selectedId, setSelectedId] = useState(null);
45
- const elements = history.present;
46
- const commit = useCallback((next) => dispatch({ type: "commit", elements: next }), []);
47
+ const { elements, crop } = history.present;
48
+ const commit = useCallback(
49
+ (next) => dispatch({ type: "commit", snapshot: { elements: next, crop } }),
50
+ [crop]
51
+ );
52
+ const setCrop = useCallback(
53
+ (next) => dispatch({ type: "commit", snapshot: { elements, crop: next } }),
54
+ [elements]
55
+ );
47
56
  const addElement = useCallback(
48
57
  (element, options) => {
49
- dispatch({ type: "commit", elements: [...elements, element] });
58
+ commit([...elements, element]);
50
59
  if (options?.select !== false) setSelectedId(element.id);
51
60
  },
52
- [elements]
61
+ [elements, commit]
53
62
  );
54
63
  const updateElement = useCallback(
55
64
  (id, updater) => {
@@ -118,17 +127,19 @@ const useAnnotationDocument = (size, initialElements = []) => {
118
127
  [selectedId, updateElement]
119
128
  );
120
129
  const document = useMemo(
121
- () => ({ version: ANNOTATION_DOCUMENT_VERSION, width: size.width, height: size.height, elements }),
122
- [size.width, size.height, elements]
130
+ () => ({ version: ANNOTATION_DOCUMENT_VERSION, width: size.width, height: size.height, elements, crop }),
131
+ [size.width, size.height, elements, crop]
123
132
  );
124
133
  return {
125
134
  elements,
135
+ crop,
126
136
  document,
127
137
  selectedId,
128
138
  selectedElement,
129
139
  canUndo: history.past.length > 0,
130
140
  canRedo: history.future.length > 0,
131
- isEmpty: elements.length === 0,
141
+ isEmpty: elements.length === 0 && !crop,
142
+ setCrop,
132
143
  select: setSelectedId,
133
144
  addElement,
134
145
  updateElement,
@@ -1 +1 @@
1
- {"version":3,"file":"useAnnotationDocument.js","sources":["../../../../../src/components/UI/ImageAnnotator/hooks/useAnnotationDocument.ts"],"sourcesContent":["import { useCallback, useMemo, useReducer, useState } from 'react';\n\nimport { duplicateElement, reorderElements, translateElement } from '../helpers/annotationElements';\nimport {\n ANNOTATION_DOCUMENT_VERSION,\n IAnnotationDocument,\n IAnnotationSize,\n TAnnotationElement,\n} from '../interfaces/annotationDocument';\n\nconst HISTORY_LIMIT = 50;\n\ntype TElementList = Array<TAnnotationElement>;\n\ninterface IHistoryState {\n past: Array<TElementList>;\n present: TElementList;\n future: Array<TElementList>;\n}\n\ntype THistoryAction =\n | { type: 'commit'; elements: TElementList }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'reset'; elements: TElementList };\n\n/**\n * Snapshots instead of inverse commands: an annotation document holds tens of elements, and a\n * snapshot stack is the only shape where undo and redo cannot drift apart.\n */\nconst historyReducer = (state: IHistoryState, action: THistoryAction): IHistoryState => {\n switch (action.type) {\n case 'commit':\n if (action.elements === state.present) return state;\n return {\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: action.elements,\n future: [],\n };\n case 'undo': {\n const previous = state.past[state.past.length - 1];\n if (!previous) return state;\n return {\n past: state.past.slice(0, -1),\n present: previous,\n future: [state.present, ...state.future].slice(0, HISTORY_LIMIT),\n };\n }\n case 'redo': {\n const [next, ...rest] = state.future;\n if (!next) return state;\n return {\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: next,\n future: rest,\n };\n }\n case 'reset':\n return { past: [], present: action.elements, future: [] };\n default:\n return state;\n }\n};\n\nexport interface IAnnotationDocumentController {\n elements: TElementList;\n document: IAnnotationDocument;\n selectedId: string | null;\n selectedElement: TAnnotationElement | null;\n canUndo: boolean;\n canRedo: boolean;\n isEmpty: boolean;\n select: (id: string | null) => void;\n addElement: (element: TAnnotationElement, options?: { select?: boolean }) => void;\n updateElement: (id: string, updater: (element: TAnnotationElement) => TAnnotationElement) => void;\n updateSelected: (updater: (element: TAnnotationElement) => TAnnotationElement) => void;\n removeElement: (id: string) => void;\n removeElements: (ids: Array<string>) => void;\n duplicateSelected: () => void;\n nudgeSelected: (deltaX: number, deltaY: number) => void;\n reorderSelected: (direction: 'forward' | 'backward' | 'front' | 'back') => void;\n undo: () => void;\n redo: () => void;\n clear: () => void;\n}\n\nexport const useAnnotationDocument = (\n size: IAnnotationSize,\n initialElements: TElementList = [],\n): IAnnotationDocumentController => {\n const [history, dispatch] = useReducer(historyReducer, {\n past: [],\n present: initialElements,\n future: [],\n });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const elements = history.present;\n\n const commit = useCallback((next: TElementList) => dispatch({ type: 'commit', elements: next }), []);\n\n const addElement = useCallback(\n (element: TAnnotationElement, options?: { select?: boolean }) => {\n dispatch({ type: 'commit', elements: [...elements, element] });\n if (options?.select !== false) setSelectedId(element.id);\n },\n [elements],\n );\n\n const updateElement = useCallback(\n (id: string, updater: (element: TAnnotationElement) => TAnnotationElement) => {\n const index = elements.findIndex((element) => element.id === id);\n if (index < 0) return;\n\n const next = [...elements];\n next[index] = updater(next[index]);\n commit(next);\n },\n [elements, commit],\n );\n\n const removeElements = useCallback(\n (ids: Array<string>) => {\n if (!ids.length) return;\n const removed = new Set(ids);\n const next = elements.filter((element) => !removed.has(element.id));\n if (next.length === elements.length) return;\n\n commit(next);\n setSelectedId((current) => (current && removed.has(current) ? null : current));\n },\n [elements, commit],\n );\n\n const selectedElement = useMemo(\n () => elements.find((element) => element.id === selectedId) ?? null,\n [elements, selectedId],\n );\n\n const undo = useCallback(() => {\n dispatch({ type: 'undo' });\n setSelectedId(null);\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'redo' });\n setSelectedId(null);\n }, []);\n\n const clear = useCallback(() => {\n if (!elements.length) return;\n commit([]);\n setSelectedId(null);\n }, [elements.length, commit]);\n\n const duplicateSelected = useCallback(() => {\n if (!selectedElement) return;\n const offset = Math.max(8, Math.round(Math.max(size.width, size.height) * 0.02));\n const copy = duplicateElement(selectedElement, offset);\n commit([...elements, copy]);\n setSelectedId(copy.id);\n }, [selectedElement, elements, commit, size.width, size.height]);\n\n const nudgeSelected = useCallback(\n (deltaX: number, deltaY: number) => {\n if (!selectedElement) return;\n updateElement(selectedElement.id, (element) => translateElement(element, deltaX, deltaY));\n },\n [selectedElement, updateElement],\n );\n\n const reorderSelected = useCallback(\n (direction: 'forward' | 'backward' | 'front' | 'back') => {\n if (!selectedId) return;\n commit(reorderElements(elements, selectedId, direction));\n },\n [selectedId, elements, commit],\n );\n\n const updateSelected = useCallback(\n (updater: (element: TAnnotationElement) => TAnnotationElement) => {\n if (!selectedId) return;\n updateElement(selectedId, updater);\n },\n [selectedId, updateElement],\n );\n\n const document = useMemo<IAnnotationDocument>(\n () => ({ version: ANNOTATION_DOCUMENT_VERSION, width: size.width, height: size.height, elements }),\n [size.width, size.height, elements],\n );\n\n return {\n elements,\n document,\n selectedId,\n selectedElement,\n canUndo: history.past.length > 0,\n canRedo: history.future.length > 0,\n isEmpty: elements.length === 0,\n select: setSelectedId,\n addElement,\n updateElement,\n updateSelected,\n removeElement: useCallback((id: string) => removeElements([id]), [removeElements]),\n removeElements,\n duplicateSelected,\n nudgeSelected,\n reorderSelected,\n undo,\n redo,\n clear,\n };\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAnnotationDocument.js","sources":["../../../../../src/components/UI/ImageAnnotator/hooks/useAnnotationDocument.ts"],"sourcesContent":["import { useCallback, useMemo, useReducer, useState } from 'react';\n\nimport { duplicateElement, reorderElements, translateElement } from '../helpers/annotationElements';\nimport {\n ANNOTATION_DOCUMENT_VERSION,\n IAnnotationCropRect,\n IAnnotationDocument,\n IAnnotationSize,\n TAnnotationElement,\n} from '../interfaces/annotationDocument';\n\nconst HISTORY_LIMIT = 50;\n\ntype TElementList = Array<TAnnotationElement>;\n\n/** The crop shares the element history, so undo walks back a crop exactly like a stroke. */\ninterface IDocumentSnapshot {\n elements: TElementList;\n crop: IAnnotationCropRect | null;\n}\n\ninterface IHistoryState {\n past: Array<IDocumentSnapshot>;\n present: IDocumentSnapshot;\n future: Array<IDocumentSnapshot>;\n}\n\ntype THistoryAction =\n | { type: 'commit'; snapshot: IDocumentSnapshot }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'reset'; snapshot: IDocumentSnapshot };\n\n/**\n * Snapshots instead of inverse commands: an annotation document holds tens of elements, and a\n * snapshot stack is the only shape where undo and redo cannot drift apart.\n */\nconst historyReducer = (state: IHistoryState, action: THistoryAction): IHistoryState => {\n switch (action.type) {\n case 'commit':\n if (action.snapshot.elements === state.present.elements && action.snapshot.crop === state.present.crop) {\n return state;\n }\n return {\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: action.snapshot,\n future: [],\n };\n case 'undo': {\n const previous = state.past[state.past.length - 1];\n if (!previous) return state;\n return {\n past: state.past.slice(0, -1),\n present: previous,\n future: [state.present, ...state.future].slice(0, HISTORY_LIMIT),\n };\n }\n case 'redo': {\n const [next, ...rest] = state.future;\n if (!next) return state;\n return {\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: next,\n future: rest,\n };\n }\n case 'reset':\n return { past: [], present: action.snapshot, future: [] };\n default:\n return state;\n }\n};\n\nexport interface IAnnotationDocumentController {\n elements: TElementList;\n crop: IAnnotationCropRect | null;\n document: IAnnotationDocument;\n selectedId: string | null;\n selectedElement: TAnnotationElement | null;\n canUndo: boolean;\n canRedo: boolean;\n isEmpty: boolean;\n setCrop: (crop: IAnnotationCropRect | null) => void;\n select: (id: string | null) => void;\n addElement: (element: TAnnotationElement, options?: { select?: boolean }) => void;\n updateElement: (id: string, updater: (element: TAnnotationElement) => TAnnotationElement) => void;\n updateSelected: (updater: (element: TAnnotationElement) => TAnnotationElement) => void;\n removeElement: (id: string) => void;\n removeElements: (ids: Array<string>) => void;\n duplicateSelected: () => void;\n nudgeSelected: (deltaX: number, deltaY: number) => void;\n reorderSelected: (direction: 'forward' | 'backward' | 'front' | 'back') => void;\n undo: () => void;\n redo: () => void;\n clear: () => void;\n}\n\nexport const useAnnotationDocument = (\n size: IAnnotationSize,\n initialElements: TElementList = [],\n initialCrop: IAnnotationCropRect | null = null,\n): IAnnotationDocumentController => {\n const [history, dispatch] = useReducer(historyReducer, {\n past: [],\n present: { elements: initialElements, crop: initialCrop },\n future: [],\n });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const { elements, crop } = history.present;\n\n const commit = useCallback(\n (next: TElementList) => dispatch({ type: 'commit', snapshot: { elements: next, crop } }),\n [crop],\n );\n\n const setCrop = useCallback(\n (next: IAnnotationCropRect | null) => dispatch({ type: 'commit', snapshot: { elements, crop: next } }),\n [elements],\n );\n\n const addElement = useCallback(\n (element: TAnnotationElement, options?: { select?: boolean }) => {\n commit([...elements, element]);\n if (options?.select !== false) setSelectedId(element.id);\n },\n [elements, commit],\n );\n\n const updateElement = useCallback(\n (id: string, updater: (element: TAnnotationElement) => TAnnotationElement) => {\n const index = elements.findIndex((element) => element.id === id);\n if (index < 0) return;\n\n const next = [...elements];\n next[index] = updater(next[index]);\n commit(next);\n },\n [elements, commit],\n );\n\n const removeElements = useCallback(\n (ids: Array<string>) => {\n if (!ids.length) return;\n const removed = new Set(ids);\n const next = elements.filter((element) => !removed.has(element.id));\n if (next.length === elements.length) return;\n\n commit(next);\n setSelectedId((current) => (current && removed.has(current) ? null : current));\n },\n [elements, commit],\n );\n\n const selectedElement = useMemo(\n () => elements.find((element) => element.id === selectedId) ?? null,\n [elements, selectedId],\n );\n\n const undo = useCallback(() => {\n dispatch({ type: 'undo' });\n setSelectedId(null);\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'redo' });\n setSelectedId(null);\n }, []);\n\n const clear = useCallback(() => {\n if (!elements.length) return;\n commit([]);\n setSelectedId(null);\n }, [elements.length, commit]);\n\n const duplicateSelected = useCallback(() => {\n if (!selectedElement) return;\n const offset = Math.max(8, Math.round(Math.max(size.width, size.height) * 0.02));\n const copy = duplicateElement(selectedElement, offset);\n commit([...elements, copy]);\n setSelectedId(copy.id);\n }, [selectedElement, elements, commit, size.width, size.height]);\n\n const nudgeSelected = useCallback(\n (deltaX: number, deltaY: number) => {\n if (!selectedElement) return;\n updateElement(selectedElement.id, (element) => translateElement(element, deltaX, deltaY));\n },\n [selectedElement, updateElement],\n );\n\n const reorderSelected = useCallback(\n (direction: 'forward' | 'backward' | 'front' | 'back') => {\n if (!selectedId) return;\n commit(reorderElements(elements, selectedId, direction));\n },\n [selectedId, elements, commit],\n );\n\n const updateSelected = useCallback(\n (updater: (element: TAnnotationElement) => TAnnotationElement) => {\n if (!selectedId) return;\n updateElement(selectedId, updater);\n },\n [selectedId, updateElement],\n );\n\n const document = useMemo<IAnnotationDocument>(\n () => ({ version: ANNOTATION_DOCUMENT_VERSION, width: size.width, height: size.height, elements, crop }),\n [size.width, size.height, elements, crop],\n );\n\n return {\n elements,\n crop,\n document,\n selectedId,\n selectedElement,\n canUndo: history.past.length > 0,\n canRedo: history.future.length > 0,\n isEmpty: elements.length === 0 && !crop,\n setCrop,\n select: setSelectedId,\n addElement,\n updateElement,\n updateSelected,\n removeElement: useCallback((id: string) => removeElements([id]), [removeElements]),\n removeElements,\n duplicateSelected,\n nudgeSelected,\n reorderSelected,\n undo,\n redo,\n clear,\n };\n};\n"],"names":[],"mappings":"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"}
@@ -7,7 +7,8 @@ const TOOL_KEYS = {
7
7
  l: "line",
8
8
  r: "rectangle",
9
9
  o: "ellipse",
10
- t: "text"
10
+ t: "text",
11
+ c: "crop"
11
12
  };
12
13
  const NUDGE_STEP = 1;
13
14
  const NUDGE_STEP_FAST = 10;
@@ -1 +1 @@
1
- {"version":3,"file":"useAnnotationShortcuts.js","sources":["../../../../../src/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.ts"],"sourcesContent":["import { useEffect } from 'react';\n\nimport { TAnnotationTool } from '../interfaces/annotationDocument';\n\nimport { IAnnotationDocumentController } from './useAnnotationDocument';\n\nconst TOOL_KEYS: Record<string, TAnnotationTool> = {\n v: 'select',\n p: 'freehand',\n e: 'eraser',\n a: 'arrow',\n l: 'line',\n r: 'rectangle',\n o: 'ellipse',\n t: 'text',\n};\n\nconst NUDGE_STEP = 1;\nconst NUDGE_STEP_FAST = 10;\n\nconst isTypingTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n return target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName);\n};\n\ninterface IAnnotationShortcutsOptions {\n active: boolean;\n controller: IAnnotationDocumentController;\n imageMaxDimension: number;\n onToolChange: (tool: TAnnotationTool) => void;\n}\n\n/**\n * Capture phase on purpose: Escape must clear the selection without also reaching the dialog's\n * own dismiss handler, which would close the editor on the first press.\n */\nexport const useAnnotationShortcuts = ({\n active,\n controller,\n imageMaxDimension,\n onToolChange,\n}: IAnnotationShortcutsOptions): void => {\n useEffect(() => {\n if (!active) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (isTypingTarget(event.target)) return;\n\n const isAccelerator = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (isAccelerator && key === 'z') {\n event.preventDefault();\n if (event.shiftKey) controller.redo();\n else controller.undo();\n return;\n }\n\n if (isAccelerator && key === 'y') {\n event.preventDefault();\n controller.redo();\n return;\n }\n\n if (isAccelerator && key === 'd') {\n event.preventDefault();\n controller.duplicateSelected();\n return;\n }\n\n if (isAccelerator) return;\n\n if (event.key === 'Escape' && controller.selectedId) {\n event.preventDefault();\n event.stopPropagation();\n controller.select(null);\n return;\n }\n\n if ((event.key === 'Delete' || event.key === 'Backspace') && controller.selectedId) {\n event.preventDefault();\n controller.removeElement(controller.selectedId);\n return;\n }\n\n if (event.key.startsWith('Arrow') && controller.selectedId) {\n event.preventDefault();\n const step = (event.shiftKey ? NUDGE_STEP_FAST : NUDGE_STEP) * Math.max(1, Math.round(imageMaxDimension / 400));\n const deltaX = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;\n const deltaY = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;\n controller.nudgeSelected(deltaX, deltaY);\n return;\n }\n\n const tool = TOOL_KEYS[key];\n if (tool) {\n event.preventDefault();\n onToolChange(tool);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown, true);\n return () => window.removeEventListener('keydown', handleKeyDown, true);\n }, [active, controller, imageMaxDimension, onToolChange]);\n};\n"],"names":[],"mappings":";AAMA,MAAM,YAA6C;AAAA,EACjD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAExB,MAAM,iBAAiB,CAAC,WAAwC;AAC9D,MAAI,EAAE,kBAAkB,aAAc,QAAO;AAC7C,SAAO,OAAO,qBAAqB,CAAC,SAAS,YAAY,QAAQ,EAAE,SAAS,OAAO,OAAO;AAC5F;AAaO,MAAM,yBAAyB,CAAC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAyC;AACvC,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AAEb,UAAM,gBAAgB,CAAC,UAAyB;AAC9C,UAAI,eAAe,MAAM,MAAM,EAAG;AAElC,YAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,YAAM,MAAM,MAAM,IAAI,YAAA;AAEtB,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,YAAI,MAAM,SAAU,YAAW,KAAA;AAAA,wBACf,KAAA;AAChB;AAAA,MACF;AAEA,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,mBAAW,KAAA;AACX;AAAA,MACF;AAEA,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,mBAAW,kBAAA;AACX;AAAA,MACF;AAEA,UAAI,cAAe;AAEnB,UAAI,MAAM,QAAQ,YAAY,WAAW,YAAY;AACnD,cAAM,eAAA;AACN,cAAM,gBAAA;AACN,mBAAW,OAAO,IAAI;AACtB;AAAA,MACF;AAEA,WAAK,MAAM,QAAQ,YAAY,MAAM,QAAQ,gBAAgB,WAAW,YAAY;AAClF,cAAM,eAAA;AACN,mBAAW,cAAc,WAAW,UAAU;AAC9C;AAAA,MACF;AAEA,UAAI,MAAM,IAAI,WAAW,OAAO,KAAK,WAAW,YAAY;AAC1D,cAAM,eAAA;AACN,cAAM,QAAQ,MAAM,WAAW,kBAAkB,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,oBAAoB,GAAG,CAAC;AAC9G,cAAM,SAAS,MAAM,QAAQ,cAAc,CAAC,OAAO,MAAM,QAAQ,eAAe,OAAO;AACvF,cAAM,SAAS,MAAM,QAAQ,YAAY,CAAC,OAAO,MAAM,QAAQ,cAAc,OAAO;AACpF,mBAAW,cAAc,QAAQ,MAAM;AACvC;AAAA,MACF;AAEA,YAAM,OAAO,UAAU,GAAG;AAC1B,UAAI,MAAM;AACR,cAAM,eAAA;AACN,qBAAa,IAAI;AAAA,MACnB;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,eAAe,IAAI;AACtD,WAAO,MAAM,OAAO,oBAAoB,WAAW,eAAe,IAAI;AAAA,EACxE,GAAG,CAAC,QAAQ,YAAY,mBAAmB,YAAY,CAAC;AAC1D;"}
1
+ {"version":3,"file":"useAnnotationShortcuts.js","sources":["../../../../../src/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.ts"],"sourcesContent":["import { useEffect } from 'react';\n\nimport { TAnnotationTool } from '../interfaces/annotationDocument';\n\nimport { IAnnotationDocumentController } from './useAnnotationDocument';\n\nconst TOOL_KEYS: Record<string, TAnnotationTool> = {\n v: 'select',\n p: 'freehand',\n e: 'eraser',\n a: 'arrow',\n l: 'line',\n r: 'rectangle',\n o: 'ellipse',\n t: 'text',\n c: 'crop',\n};\n\nconst NUDGE_STEP = 1;\nconst NUDGE_STEP_FAST = 10;\n\nconst isTypingTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n return target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName);\n};\n\ninterface IAnnotationShortcutsOptions {\n active: boolean;\n controller: IAnnotationDocumentController;\n imageMaxDimension: number;\n onToolChange: (tool: TAnnotationTool) => void;\n}\n\n/**\n * Capture phase on purpose: Escape must clear the selection without also reaching the dialog's\n * own dismiss handler, which would close the editor on the first press.\n */\nexport const useAnnotationShortcuts = ({\n active,\n controller,\n imageMaxDimension,\n onToolChange,\n}: IAnnotationShortcutsOptions): void => {\n useEffect(() => {\n if (!active) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (isTypingTarget(event.target)) return;\n\n const isAccelerator = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (isAccelerator && key === 'z') {\n event.preventDefault();\n if (event.shiftKey) controller.redo();\n else controller.undo();\n return;\n }\n\n if (isAccelerator && key === 'y') {\n event.preventDefault();\n controller.redo();\n return;\n }\n\n if (isAccelerator && key === 'd') {\n event.preventDefault();\n controller.duplicateSelected();\n return;\n }\n\n if (isAccelerator) return;\n\n if (event.key === 'Escape' && controller.selectedId) {\n event.preventDefault();\n event.stopPropagation();\n controller.select(null);\n return;\n }\n\n if ((event.key === 'Delete' || event.key === 'Backspace') && controller.selectedId) {\n event.preventDefault();\n controller.removeElement(controller.selectedId);\n return;\n }\n\n if (event.key.startsWith('Arrow') && controller.selectedId) {\n event.preventDefault();\n const step = (event.shiftKey ? NUDGE_STEP_FAST : NUDGE_STEP) * Math.max(1, Math.round(imageMaxDimension / 400));\n const deltaX = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;\n const deltaY = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;\n controller.nudgeSelected(deltaX, deltaY);\n return;\n }\n\n const tool = TOOL_KEYS[key];\n if (tool) {\n event.preventDefault();\n onToolChange(tool);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown, true);\n return () => window.removeEventListener('keydown', handleKeyDown, true);\n }, [active, controller, imageMaxDimension, onToolChange]);\n};\n"],"names":[],"mappings":";AAMA,MAAM,YAA6C;AAAA,EACjD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAExB,MAAM,iBAAiB,CAAC,WAAwC;AAC9D,MAAI,EAAE,kBAAkB,aAAc,QAAO;AAC7C,SAAO,OAAO,qBAAqB,CAAC,SAAS,YAAY,QAAQ,EAAE,SAAS,OAAO,OAAO;AAC5F;AAaO,MAAM,yBAAyB,CAAC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAyC;AACvC,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AAEb,UAAM,gBAAgB,CAAC,UAAyB;AAC9C,UAAI,eAAe,MAAM,MAAM,EAAG;AAElC,YAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,YAAM,MAAM,MAAM,IAAI,YAAA;AAEtB,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,YAAI,MAAM,SAAU,YAAW,KAAA;AAAA,wBACf,KAAA;AAChB;AAAA,MACF;AAEA,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,mBAAW,KAAA;AACX;AAAA,MACF;AAEA,UAAI,iBAAiB,QAAQ,KAAK;AAChC,cAAM,eAAA;AACN,mBAAW,kBAAA;AACX;AAAA,MACF;AAEA,UAAI,cAAe;AAEnB,UAAI,MAAM,QAAQ,YAAY,WAAW,YAAY;AACnD,cAAM,eAAA;AACN,cAAM,gBAAA;AACN,mBAAW,OAAO,IAAI;AACtB;AAAA,MACF;AAEA,WAAK,MAAM,QAAQ,YAAY,MAAM,QAAQ,gBAAgB,WAAW,YAAY;AAClF,cAAM,eAAA;AACN,mBAAW,cAAc,WAAW,UAAU;AAC9C;AAAA,MACF;AAEA,UAAI,MAAM,IAAI,WAAW,OAAO,KAAK,WAAW,YAAY;AAC1D,cAAM,eAAA;AACN,cAAM,QAAQ,MAAM,WAAW,kBAAkB,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,oBAAoB,GAAG,CAAC;AAC9G,cAAM,SAAS,MAAM,QAAQ,cAAc,CAAC,OAAO,MAAM,QAAQ,eAAe,OAAO;AACvF,cAAM,SAAS,MAAM,QAAQ,YAAY,CAAC,OAAO,MAAM,QAAQ,cAAc,OAAO;AACpF,mBAAW,cAAc,QAAQ,MAAM;AACvC;AAAA,MACF;AAEA,YAAM,OAAO,UAAU,GAAG;AAC1B,UAAI,MAAM;AACR,cAAM,eAAA;AACN,qBAAa,IAAI;AAAA,MACnB;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,eAAe,IAAI;AACtD,WAAO,MAAM,OAAO,oBAAoB,WAAW,eAAe,IAAI;AAAA,EACxE,GAAG,CAAC,QAAQ,YAAY,mBAAmB,YAAY,CAAC;AAC1D;"}
@@ -1,5 +1,5 @@
1
1
  export type TAnnotationElementType = 'freehand' | 'line' | 'arrow' | 'rectangle' | 'ellipse' | 'text' | 'image';
2
- export type TAnnotationTool = 'select' | 'freehand' | 'eraser' | TAnnotationElementType;
2
+ export type TAnnotationTool = 'select' | 'freehand' | 'eraser' | 'crop' | TAnnotationElementType;
3
3
  export type TAnnotationAlign = 'left' | 'center' | 'right';
4
4
  export interface IAnnotationPoint {
5
5
  x: number;
@@ -9,6 +9,13 @@ export interface IAnnotationSize {
9
9
  width: number;
10
10
  height: number;
11
11
  }
12
+ /** Visible region of the base image, in native image pixels. */
13
+ export interface IAnnotationCropRect {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
12
19
  interface IAnnotationElementBase {
13
20
  id: string;
14
21
  rotation: number;
@@ -80,6 +87,8 @@ export interface IAnnotationDocument {
80
87
  width: number;
81
88
  height: number;
82
89
  elements: Array<TAnnotationElement>;
90
+ /** `null` shows the whole image; elements keep full-image coordinates either way. */
91
+ crop?: IAnnotationCropRect | null;
83
92
  }
84
93
  export type TAnnotationElementPatch = Partial<Omit<TAnnotationElement, 'id' | 'type'>>;
85
94
  export declare const isStrokeElement: (element: TAnnotationElement) => element is IAnnotationFreehandElement | IAnnotationSegmentElement | IAnnotationShapeElement;
@@ -1 +1 @@
1
- {"version":3,"file":"annotationDocument.js","sources":["../../../../../src/components/UI/ImageAnnotator/interfaces/annotationDocument.ts"],"sourcesContent":["export type TAnnotationElementType = 'freehand' | 'line' | 'arrow' | 'rectangle' | 'ellipse' | 'text' | 'image';\n\nexport type TAnnotationTool = 'select' | 'freehand' | 'eraser' | TAnnotationElementType;\n\nexport type TAnnotationAlign = 'left' | 'center' | 'right';\n\nexport interface IAnnotationPoint {\n x: number;\n y: number;\n}\n\nexport interface IAnnotationSize {\n width: number;\n height: number;\n}\n\ninterface IAnnotationElementBase {\n id: string;\n rotation: number;\n opacity: number;\n}\n\nexport interface IAnnotationFreehandElement extends IAnnotationElementBase {\n type: 'freehand';\n x: number;\n y: number;\n /** Flat `[x0, y0, x1, y1, ...]` in element space, so `x`/`y` stay the drag anchor. */\n points: Array<number>;\n stroke: string;\n strokeWidth: number;\n}\n\nexport interface IAnnotationSegmentElement extends IAnnotationElementBase {\n type: 'line' | 'arrow';\n start: IAnnotationPoint;\n end: IAnnotationPoint;\n stroke: string;\n strokeWidth: number;\n}\n\nexport interface IAnnotationShapeElement extends IAnnotationElementBase {\n type: 'rectangle' | 'ellipse';\n x: number;\n y: number;\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n fill: string | null;\n fillOpacity: number;\n cornerRadius: number;\n}\n\nexport interface IAnnotationTextElement extends IAnnotationElementBase {\n type: 'text';\n x: number;\n y: number;\n width: number;\n text: string;\n fontSize: number;\n bold: boolean;\n italic: boolean;\n align: TAnnotationAlign;\n color: string;\n background: string | null;\n backgroundOpacity: number;\n padding: number;\n cornerRadius: number;\n}\n\nexport interface IAnnotationImageElement extends IAnnotationElementBase {\n type: 'image';\n x: number;\n y: number;\n width: number;\n height: number;\n src: string;\n naturalWidth: number;\n naturalHeight: number;\n cornerRadius: number;\n stroke: string | null;\n strokeWidth: number;\n shadow: boolean;\n}\n\nexport type TAnnotationElement =\n | IAnnotationFreehandElement\n | IAnnotationSegmentElement\n | IAnnotationShapeElement\n | IAnnotationTextElement\n | IAnnotationImageElement;\n\nexport const ANNOTATION_DOCUMENT_VERSION = 1;\n\nexport interface IAnnotationDocument {\n version: number;\n /** Element coordinates live in this space, never in screen pixels. */\n width: number;\n height: number;\n elements: Array<TAnnotationElement>;\n}\n\nexport type TAnnotationElementPatch = Partial<Omit<TAnnotationElement, 'id' | 'type'>>;\n\nexport const isStrokeElement = (\n element: TAnnotationElement,\n): element is IAnnotationFreehandElement | IAnnotationSegmentElement | IAnnotationShapeElement =>\n element.type !== 'text' && element.type !== 'image';\n\nexport const isFillableElement = (element: TAnnotationElement): element is IAnnotationShapeElement =>\n element.type === 'rectangle' || element.type === 'ellipse';\n\nexport const isSegmentElement = (element: TAnnotationElement): element is IAnnotationSegmentElement =>\n element.type === 'line' || element.type === 'arrow';\n\nexport const isBoxElement = (\n element: TAnnotationElement,\n): element is IAnnotationShapeElement | IAnnotationImageElement =>\n element.type === 'rectangle' || element.type === 'ellipse' || element.type === 'image';\n"],"names":[],"mappings":"AA4FO,MAAM,8BAA8B;AAYpC,MAAM,kBAAkB,CAC7B,YAEA,QAAQ,SAAS,UAAU,QAAQ,SAAS;AAEvC,MAAM,oBAAoB,CAAC,YAChC,QAAQ,SAAS,eAAe,QAAQ,SAAS;AAE5C,MAAM,mBAAmB,CAAC,YAC/B,QAAQ,SAAS,UAAU,QAAQ,SAAS;AAEvC,MAAM,eAAe,CAC1B,YAEA,QAAQ,SAAS,eAAe,QAAQ,SAAS,aAAa,QAAQ,SAAS;"}
1
+ {"version":3,"file":"annotationDocument.js","sources":["../../../../../src/components/UI/ImageAnnotator/interfaces/annotationDocument.ts"],"sourcesContent":["export type TAnnotationElementType = 'freehand' | 'line' | 'arrow' | 'rectangle' | 'ellipse' | 'text' | 'image';\n\nexport type TAnnotationTool = 'select' | 'freehand' | 'eraser' | 'crop' | TAnnotationElementType;\n\nexport type TAnnotationAlign = 'left' | 'center' | 'right';\n\nexport interface IAnnotationPoint {\n x: number;\n y: number;\n}\n\nexport interface IAnnotationSize {\n width: number;\n height: number;\n}\n\n/** Visible region of the base image, in native image pixels. */\nexport interface IAnnotationCropRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface IAnnotationElementBase {\n id: string;\n rotation: number;\n opacity: number;\n}\n\nexport interface IAnnotationFreehandElement extends IAnnotationElementBase {\n type: 'freehand';\n x: number;\n y: number;\n /** Flat `[x0, y0, x1, y1, ...]` in element space, so `x`/`y` stay the drag anchor. */\n points: Array<number>;\n stroke: string;\n strokeWidth: number;\n}\n\nexport interface IAnnotationSegmentElement extends IAnnotationElementBase {\n type: 'line' | 'arrow';\n start: IAnnotationPoint;\n end: IAnnotationPoint;\n stroke: string;\n strokeWidth: number;\n}\n\nexport interface IAnnotationShapeElement extends IAnnotationElementBase {\n type: 'rectangle' | 'ellipse';\n x: number;\n y: number;\n width: number;\n height: number;\n stroke: string;\n strokeWidth: number;\n fill: string | null;\n fillOpacity: number;\n cornerRadius: number;\n}\n\nexport interface IAnnotationTextElement extends IAnnotationElementBase {\n type: 'text';\n x: number;\n y: number;\n width: number;\n text: string;\n fontSize: number;\n bold: boolean;\n italic: boolean;\n align: TAnnotationAlign;\n color: string;\n background: string | null;\n backgroundOpacity: number;\n padding: number;\n cornerRadius: number;\n}\n\nexport interface IAnnotationImageElement extends IAnnotationElementBase {\n type: 'image';\n x: number;\n y: number;\n width: number;\n height: number;\n src: string;\n naturalWidth: number;\n naturalHeight: number;\n cornerRadius: number;\n stroke: string | null;\n strokeWidth: number;\n shadow: boolean;\n}\n\nexport type TAnnotationElement =\n | IAnnotationFreehandElement\n | IAnnotationSegmentElement\n | IAnnotationShapeElement\n | IAnnotationTextElement\n | IAnnotationImageElement;\n\nexport const ANNOTATION_DOCUMENT_VERSION = 1;\n\nexport interface IAnnotationDocument {\n version: number;\n /** Element coordinates live in this space, never in screen pixels. */\n width: number;\n height: number;\n elements: Array<TAnnotationElement>;\n /** `null` shows the whole image; elements keep full-image coordinates either way. */\n crop?: IAnnotationCropRect | null;\n}\n\nexport type TAnnotationElementPatch = Partial<Omit<TAnnotationElement, 'id' | 'type'>>;\n\nexport const isStrokeElement = (\n element: TAnnotationElement,\n): element is IAnnotationFreehandElement | IAnnotationSegmentElement | IAnnotationShapeElement =>\n element.type !== 'text' && element.type !== 'image';\n\nexport const isFillableElement = (element: TAnnotationElement): element is IAnnotationShapeElement =>\n element.type === 'rectangle' || element.type === 'ellipse';\n\nexport const isSegmentElement = (element: TAnnotationElement): element is IAnnotationSegmentElement =>\n element.type === 'line' || element.type === 'arrow';\n\nexport const isBoxElement = (\n element: TAnnotationElement,\n): element is IAnnotationShapeElement | IAnnotationImageElement =>\n element.type === 'rectangle' || element.type === 'ellipse' || element.type === 'image';\n"],"names":[],"mappings":"AAoGO,MAAM,8BAA8B;AAcpC,MAAM,kBAAkB,CAC7B,YAEA,QAAQ,SAAS,UAAU,QAAQ,SAAS;AAEvC,MAAM,oBAAoB,CAAC,YAChC,QAAQ,SAAS,eAAe,QAAQ,SAAS;AAE5C,MAAM,mBAAmB,CAAC,YAC/B,QAAQ,SAAS,UAAU,QAAQ,SAAS;AAEvC,MAAM,eAAe,CAC1B,YAEA,QAAQ,SAAS,eAAe,QAAQ,SAAS,aAAa,QAAQ,SAAS;"}
@@ -424,7 +424,8 @@ declare const _default: {
424
424
  "rectangle": "Rectangle",
425
425
  "ellipse": "Ellipse",
426
426
  "text": "Text",
427
- "image": "Insert image"
427
+ "image": "Insert image",
428
+ "crop": "Crop"
428
429
  },
429
430
  "stroke": {
430
431
  "thin": "Thin stroke",
@@ -464,7 +465,21 @@ declare const _default: {
464
465
  "imageInsertFailed": "Could not insert the selected image",
465
466
  "sendFailed": "Could not upload the annotated image",
466
467
  "save": "Save annotations",
467
- "saving": "Saving..."
468
+ "saving": "Saving...",
469
+ "crop": {
470
+ "aspectLabel": "Aspect ratio",
471
+ "aspect": {
472
+ "free": "Free",
473
+ "original": "Original",
474
+ "square": "1:1",
475
+ "portrait": "4:5",
476
+ "classic": "3:4",
477
+ "wide": "16:9"
478
+ },
479
+ "reset": "Restore full image",
480
+ "cancel": "Cancel",
481
+ "apply": "Apply crop"
482
+ }
468
483
  },
469
484
  "imageZoom": {
470
485
  "title": "Enlarged image",
@@ -18,7 +18,7 @@ const generatedByAI = { "tooltip": "Generated by AI" };
18
18
  const imageAiAdjust = { "action": "Adjust with AI", "title": "Adjust image with AI", "subtitle": "Describe the adjustment. The AI changes only what you ask and preserves the rest of the image.", "original": "Original", "result": "Result", "resultHint": "Describe the adjustment and click Generate to see the result here.", "promptLabel": "Desired adjustment", "promptPlaceholder": "E.g. remove the background and make it white, smooth the sleeve creases, brighten the lighting", "generate": "Generate", "generating": "Generating the image...", "tryAgain": "Generate another", "useResult": "Use this image", "errorGenerate": "Could not adjust the image with AI", "errorSave": "Could not save the adjusted image", "errorRefused": "The AI could not make this adjustment: {{reason}}", "mask": { "start": "Mark the area", "stop": "Use the whole image", "hint": "Paint the area the AI may change. The rest of the image is kept." }, "verification": { "changedMoreThanRequested": "The AI changed more than requested. Check the areas outside the adjustment before using it.", "colorMismatch": "The garment color ({{dominant}}) differs from the requested color ({{target}})." } };
19
19
  const imageMaskPainter = { "paint": "Paint", "erase": "Erase", "brushSize": "Brush size", "undo": "Undo", "clear": "Clear", "surfaceLabel": "Mask painting area over the image" };
20
20
  const imageAiReview = { "discardTitle": "Why are you discarding this result?", "discardSubtitle": "Your answer helps us improve the AI. The image is discarded either way.", "discardConfirm": "Discard", "reasonLabel": "Reason", "detailsLabel": "Details", "detailsPlaceholder": "Optional: tell us what went wrong", "reasons": { "changed_the_product": "Changed the product", "wrong_color": "Wrong color", "bad_quality": "Bad quality", "person_changed": "The person changed", "text_or_logo": "Text or logo problem", "not_what_i_asked": "Not what I asked for", "other": "Other" } };
21
- const imageAnnotator = { "title": "Annotate image", "annotate": "Annotate image", "tools": { "select": "Select", "pen": "Freehand pen", "eraser": "Eraser", "arrow": "Arrow", "line": "Line", "rectangle": "Rectangle", "ellipse": "Ellipse", "text": "Text", "image": "Insert image" }, "stroke": { "thin": "Thin stroke", "medium": "Medium stroke", "thick": "Thick stroke" }, "style": { "none": "No colour", "fill": "Fill", "textBackground": "Text background", "bold": "Bold", "italic": "Italic", "alignLeft": "Align left", "alignCenter": "Align centre", "alignRight": "Align right", "border": "Image border", "shadow": "Image shadow", "font": { "small": "Small text", "medium": "Medium text", "large": "Large text" } }, "layers": { "forward": "Bring forward", "backward": "Send backward" }, "actions": { "duplicate": "Duplicate", "delete": "Delete" }, "undo": "Undo", "redo": "Redo", "clear": "Clear annotations", "textPlaceholder": "Type the text", "loadError": "Could not load the image for editing", "imageInsertFailed": "Could not insert the selected image", "sendFailed": "Could not upload the annotated image", "save": "Save annotations", "saving": "Saving..." };
21
+ const imageAnnotator = { "title": "Annotate image", "annotate": "Annotate image", "tools": { "select": "Select", "pen": "Freehand pen", "eraser": "Eraser", "arrow": "Arrow", "line": "Line", "rectangle": "Rectangle", "ellipse": "Ellipse", "text": "Text", "image": "Insert image", "crop": "Crop" }, "stroke": { "thin": "Thin stroke", "medium": "Medium stroke", "thick": "Thick stroke" }, "style": { "none": "No colour", "fill": "Fill", "textBackground": "Text background", "bold": "Bold", "italic": "Italic", "alignLeft": "Align left", "alignCenter": "Align centre", "alignRight": "Align right", "border": "Image border", "shadow": "Image shadow", "font": { "small": "Small text", "medium": "Medium text", "large": "Large text" } }, "layers": { "forward": "Bring forward", "backward": "Send backward" }, "actions": { "duplicate": "Duplicate", "delete": "Delete" }, "undo": "Undo", "redo": "Redo", "clear": "Clear annotations", "textPlaceholder": "Type the text", "loadError": "Could not load the image for editing", "imageInsertFailed": "Could not insert the selected image", "sendFailed": "Could not upload the annotated image", "save": "Save annotations", "saving": "Saving...", "crop": { "aspectLabel": "Aspect ratio", "aspect": { "free": "Free", "original": "Original", "square": "1:1", "portrait": "4:5", "classic": "3:4", "wide": "16:9" }, "reset": "Restore full image", "cancel": "Cancel", "apply": "Apply crop" } };
22
22
  const imageZoom = { "title": "Enlarged image", "close": "Close" };
23
23
  const informationCard = { "defaultTitle": "Indicators Summary" };
24
24
  const inputField = { "requiredField": "This field is required", "loadingInfo": "Loading information..." };
@@ -427,6 +427,7 @@ export declare const uiLocales: {
427
427
  ellipse: string;
428
428
  text: string;
429
429
  image: string;
430
+ crop: string;
430
431
  };
431
432
  stroke: {
432
433
  thin: string;
@@ -467,6 +468,20 @@ export declare const uiLocales: {
467
468
  sendFailed: string;
468
469
  save: string;
469
470
  saving: string;
471
+ crop: {
472
+ aspectLabel: string;
473
+ aspect: {
474
+ free: string;
475
+ original: string;
476
+ square: string;
477
+ portrait: string;
478
+ classic: string;
479
+ wide: string;
480
+ };
481
+ reset: string;
482
+ cancel: string;
483
+ apply: string;
484
+ };
470
485
  };
471
486
  imageZoom: {
472
487
  title: string;
@@ -1045,6 +1060,7 @@ export declare const uiLocales: {
1045
1060
  ellipse: string;
1046
1061
  text: string;
1047
1062
  image: string;
1063
+ crop: string;
1048
1064
  };
1049
1065
  stroke: {
1050
1066
  thin: string;
@@ -1085,6 +1101,20 @@ export declare const uiLocales: {
1085
1101
  sendFailed: string;
1086
1102
  save: string;
1087
1103
  saving: string;
1104
+ crop: {
1105
+ aspectLabel: string;
1106
+ aspect: {
1107
+ free: string;
1108
+ original: string;
1109
+ square: string;
1110
+ portrait: string;
1111
+ classic: string;
1112
+ wide: string;
1113
+ };
1114
+ reset: string;
1115
+ cancel: string;
1116
+ apply: string;
1117
+ };
1088
1118
  };
1089
1119
  imageZoom: {
1090
1120
  title: string;
@@ -424,7 +424,8 @@ declare const _default: {
424
424
  "rectangle": "Retângulo",
425
425
  "ellipse": "Elipse",
426
426
  "text": "Texto",
427
- "image": "Inserir imagem"
427
+ "image": "Inserir imagem",
428
+ "crop": "Recortar"
428
429
  },
429
430
  "stroke": {
430
431
  "thin": "Traço fino",
@@ -464,7 +465,21 @@ declare const _default: {
464
465
  "imageInsertFailed": "Não foi possível inserir a imagem selecionada",
465
466
  "sendFailed": "Não foi possível enviar a imagem anotada",
466
467
  "save": "Salvar anotações",
467
- "saving": "Salvando..."
468
+ "saving": "Salvando...",
469
+ "crop": {
470
+ "aspectLabel": "Proporção",
471
+ "aspect": {
472
+ "free": "Livre",
473
+ "original": "Original",
474
+ "square": "1:1",
475
+ "portrait": "4:5",
476
+ "classic": "3:4",
477
+ "wide": "16:9"
478
+ },
479
+ "reset": "Restaurar imagem inteira",
480
+ "cancel": "Cancelar",
481
+ "apply": "Aplicar recorte"
482
+ }
468
483
  },
469
484
  "imageZoom": {
470
485
  "title": "Imagem ampliada",
@@ -18,7 +18,7 @@ const generatedByAI = { "tooltip": "Gerado por IA" };
18
18
  const imageAiAdjust = { "action": "Ajustar com IA", "title": "Ajustar imagem com IA", "subtitle": "Descreva o ajuste. A IA altera só o que você pedir e preserva o resto da imagem.", "original": "Original", "result": "Resultado", "resultHint": "Descreva o ajuste e clique em Gerar para ver o resultado aqui.", "promptLabel": "Ajuste desejado", "promptPlaceholder": "Ex.: remover o fundo e deixar branco, tirar os vincos da manga, clarear a iluminação", "generate": "Gerar", "generating": "Gerando a imagem...", "tryAgain": "Gerar outra", "useResult": "Usar esta imagem", "errorGenerate": "Não foi possível ajustar a imagem com IA", "errorSave": "Não foi possível salvar a imagem ajustada", "errorRefused": "A IA não conseguiu fazer este ajuste: {{reason}}", "mask": { "start": "Marcar a área", "stop": "Usar a imagem toda", "hint": "Pinte a área que a IA pode alterar. O resto da imagem é mantido." }, "verification": { "changedMoreThanRequested": "A IA mudou mais do que o pedido. Confira as áreas fora do ajuste antes de usar.", "colorMismatch": "A cor da peça ({{dominant}}) ficou diferente da cor pedida ({{target}})." } };
19
19
  const imageMaskPainter = { "paint": "Pintar", "erase": "Apagar", "brushSize": "Tamanho do pincel", "undo": "Desfazer", "clear": "Limpar", "surfaceLabel": "Área de pintura da máscara sobre a imagem" };
20
20
  const imageAiReview = { "discardTitle": "Por que descartar este resultado?", "discardSubtitle": "Sua resposta ajuda a melhorar a IA. A imagem é descartada de qualquer forma.", "discardConfirm": "Descartar", "reasonLabel": "Motivo", "detailsLabel": "Detalhes", "detailsPlaceholder": "Opcional: conte o que deu errado", "reasons": { "changed_the_product": "Alterou o produto", "wrong_color": "Cor errada", "bad_quality": "Baixa qualidade", "person_changed": "A pessoa mudou", "text_or_logo": "Problema com texto ou logo", "not_what_i_asked": "Não é o que pedi", "other": "Outro" } };
21
- const imageAnnotator = { "title": "Anotar imagem", "annotate": "Anotar imagem", "tools": { "select": "Selecionar", "pen": "Caneta livre", "eraser": "Borracha", "arrow": "Seta", "line": "Linha", "rectangle": "Retângulo", "ellipse": "Elipse", "text": "Texto", "image": "Inserir imagem" }, "stroke": { "thin": "Traço fino", "medium": "Traço médio", "thick": "Traço grosso" }, "style": { "none": "Sem cor", "fill": "Preenchimento", "textBackground": "Fundo do texto", "bold": "Negrito", "italic": "Itálico", "alignLeft": "Alinhar à esquerda", "alignCenter": "Centralizar", "alignRight": "Alinhar à direita", "border": "Borda da imagem", "shadow": "Sombra da imagem", "font": { "small": "Texto pequeno", "medium": "Texto médio", "large": "Texto grande" } }, "layers": { "forward": "Trazer para frente", "backward": "Enviar para trás" }, "actions": { "duplicate": "Duplicar", "delete": "Excluir" }, "undo": "Desfazer", "redo": "Refazer", "clear": "Limpar anotações", "textPlaceholder": "Digite o texto", "loadError": "Não foi possível carregar a imagem para edição", "imageInsertFailed": "Não foi possível inserir a imagem selecionada", "sendFailed": "Não foi possível enviar a imagem anotada", "save": "Salvar anotações", "saving": "Salvando..." };
21
+ const imageAnnotator = { "title": "Anotar imagem", "annotate": "Anotar imagem", "tools": { "select": "Selecionar", "pen": "Caneta livre", "eraser": "Borracha", "arrow": "Seta", "line": "Linha", "rectangle": "Retângulo", "ellipse": "Elipse", "text": "Texto", "image": "Inserir imagem", "crop": "Recortar" }, "stroke": { "thin": "Traço fino", "medium": "Traço médio", "thick": "Traço grosso" }, "style": { "none": "Sem cor", "fill": "Preenchimento", "textBackground": "Fundo do texto", "bold": "Negrito", "italic": "Itálico", "alignLeft": "Alinhar à esquerda", "alignCenter": "Centralizar", "alignRight": "Alinhar à direita", "border": "Borda da imagem", "shadow": "Sombra da imagem", "font": { "small": "Texto pequeno", "medium": "Texto médio", "large": "Texto grande" } }, "layers": { "forward": "Trazer para frente", "backward": "Enviar para trás" }, "actions": { "duplicate": "Duplicar", "delete": "Excluir" }, "undo": "Desfazer", "redo": "Refazer", "clear": "Limpar anotações", "textPlaceholder": "Digite o texto", "loadError": "Não foi possível carregar a imagem para edição", "imageInsertFailed": "Não foi possível inserir a imagem selecionada", "sendFailed": "Não foi possível enviar a imagem anotada", "save": "Salvar anotações", "saving": "Salvando...", "crop": { "aspectLabel": "Proporção", "aspect": { "free": "Livre", "original": "Original", "square": "1:1", "portrait": "4:5", "classic": "3:4", "wide": "16:9" }, "reset": "Restaurar imagem inteira", "cancel": "Cancelar", "apply": "Aplicar recorte" } };
22
22
  const imageZoom = { "title": "Imagem ampliada", "close": "Fechar" };
23
23
  const informationCard = { "defaultTitle": "Resumo dos Indicadores" };
24
24
  const inputField = { "requiredField": "Este campo é obrigatório", "loadingInfo": "Carregando informações..." };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.24.4",
3
+ "version": "0.25.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [