lost-sia 3.0.0 → 3.1.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.
@@ -3,7 +3,7 @@ import { AllowedTools } from '../types';
3
3
  export declare const ActionsData: {};
4
4
  declare const meta: {
5
5
  title: string;
6
- component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, }: {
6
+ component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, onTimeTravel, }: {
7
7
  additionalButtons?: import('react').ReactElement;
8
8
  allowedTools?: AllowedTools;
9
9
  polygonOperationResult?: import('..').PolygonOperationResult;
@@ -26,6 +26,7 @@ declare const meta: {
26
26
  onIsImageJunk?: (isJunk: boolean) => void;
27
27
  onNotification?: (notification: import('..').SIANotification) => void;
28
28
  onSelectAnnotation?: (annotation: import('../models').Annotation) => void;
29
+ onTimeTravel?: (timeTravelAction: import('..').TimeTravelChanges) => void;
29
30
  }) => import("react/jsx-runtime").JSX.Element;
30
31
  parameters: {
31
32
  layout: string;
@@ -2,13 +2,13 @@ import { StoryObj } from '@storybook/react';
2
2
  import { default as AnnotationTool } from '../../models/AnnotationTool';
3
3
  import { AnnotationSettings, UiConfig } from '../../types';
4
4
  export declare const ActionsData: {
5
- onAnnoEvent: import('@vitest/spy').Mock<(...args: any[]) => any>;
6
- onKeyDown: import('@vitest/spy').Mock<(...args: any[]) => any>;
7
- onKeyUp: import('@vitest/spy').Mock<(...args: any[]) => any>;
5
+ onAnnoEvent: any;
6
+ onKeyDown: any;
7
+ onKeyUp: any;
8
8
  };
9
9
  declare const meta: {
10
10
  title: string;
11
- component: ({ annotations, annotationSettings, defaultLabelId, image, isFullscreen, isImageJunk, isPolygonSelectionMode, polygonOperationResult, possibleLabels, preventScrolling, selectedAnnotation, selectedAnnoTool, toolbarHeight, uiConfig, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoEditing, onNotification, onRequestNewAnnoId, onSelectAnnotation, onSetIsImageJunk, onSetSelectedTool, onShouldDeleteAnno, }: {
11
+ component: ({ annotations, annotationSettings, defaultLabelId, image, isFullscreen, isImageJunk, isPolygonSelectionMode, polygonOperationResult, possibleLabels, preventScrolling, selectedAnnotation, selectedAnnoTool, toolbarHeight, uiConfig, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoEditing, onNotification, onRequestNewAnnoId, onSelectAnnotation, onSetIsImageJunk, onSetSelectedTool, onShouldDeleteAnno, onTraverseAnnotationHistory, }: {
12
12
  annotations?: import('../../models').Annotation[];
13
13
  annotationSettings: AnnotationSettings;
14
14
  defaultLabelId?: number;
@@ -33,6 +33,7 @@ declare const meta: {
33
33
  onSetIsImageJunk: (newJunkState: boolean) => void;
34
34
  onSetSelectedTool: (tool: AnnotationTool) => void;
35
35
  onShouldDeleteAnno: (internalAnnoId: number) => void;
36
+ onTraverseAnnotationHistory: (isUndo: boolean) => void;
36
37
  }) => import("react/jsx-runtime").JSX.Element;
37
38
  parameters: {
38
39
  layout: string;
@@ -2,9 +2,9 @@ import { StoryObj } from '@storybook/react';
2
2
  import { default as AnnotationTool } from '../../models/AnnotationTool';
3
3
  import { UiConfig } from '../../types';
4
4
  export declare const ActionsData: {
5
- onAnnoEvent: import('@vitest/spy').Mock<(...args: any[]) => any>;
6
- onKeyDown: import('@vitest/spy').Mock<(...args: any[]) => any>;
7
- onKeyUp: import('@vitest/spy').Mock<(...args: any[]) => any>;
5
+ onAnnoEvent: any;
6
+ onKeyDown: any;
7
+ onKeyUp: any;
8
8
  };
9
9
  declare const meta: {
10
10
  title: string;
@@ -22,9 +22,9 @@ declare const meta: {
22
22
  tags: string[];
23
23
  excludeStories: RegExp;
24
24
  args: {
25
- onAnnoEvent: import('@vitest/spy').Mock<(...args: any[]) => any>;
26
- onKeyDown: import('@vitest/spy').Mock<(...args: any[]) => any>;
27
- onKeyUp: import('@vitest/spy').Mock<(...args: any[]) => any>;
25
+ onAnnoEvent: any;
26
+ onKeyDown: any;
27
+ onKeyUp: any;
28
28
  };
29
29
  };
30
30
  export default meta;
@@ -3,7 +3,7 @@ import { Label } from '../types';
3
3
  export declare const ActionsData: {};
4
4
  declare const meta: {
5
5
  title: string;
6
- component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, }: {
6
+ component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, onTimeTravel, }: {
7
7
  additionalButtons?: import('react').ReactElement;
8
8
  allowedTools?: import('..').AllowedTools;
9
9
  polygonOperationResult?: import('..').PolygonOperationResult;
@@ -26,6 +26,7 @@ declare const meta: {
26
26
  onIsImageJunk?: (isJunk: boolean) => void;
27
27
  onNotification?: (notification: import('..').SIANotification) => void;
28
28
  onSelectAnnotation?: (annotation: import('../models').Annotation) => void;
29
+ onTimeTravel?: (timeTravelAction: import('..').TimeTravelChanges) => void;
29
30
  }) => import("react/jsx-runtime").JSX.Element;
30
31
  parameters: {
31
32
  layout: string;
@@ -56,6 +57,7 @@ declare const meta: {
56
57
  onIsImageJunk?: (isJunk: boolean) => void;
57
58
  onNotification?: (notification: import('..').SIANotification) => void;
58
59
  onSelectAnnotation?: (annotation: import('../models').Annotation) => void;
60
+ onTimeTravel?: (timeTravelAction: import('..').TimeTravelChanges) => void;
59
61
  }>) => import("react/jsx-runtime").JSX.Element)[];
60
62
  };
61
63
  export default meta;
@@ -3,7 +3,7 @@ import { UiConfig } from '../../types';
3
3
  export declare const ActionsData: {};
4
4
  declare const meta: {
5
5
  title: string;
6
- component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, }: {
6
+ component: ({ additionalButtons, allowedTools: propAllowedTools, polygonOperationResult, annotationSettings: propAnnotationSettings, uiConfig: propUiConfig, defaultAnnotationTool, defaultLabelId, image, isLoading, isPolygonSelectionMode, initialAnnotations, initialImageLabelIds, initialIsImageJunk, possibleLabels, onAnnoCreated, onAnnoCreationFinished, onAnnoChanged, onAnnoDeleted, onImageLabelsChanged, onIsImageJunk, onNotification, onSelectAnnotation, onTimeTravel, }: {
7
7
  additionalButtons?: import('react').ReactElement;
8
8
  allowedTools?: import('../..').AllowedTools;
9
9
  polygonOperationResult?: import('../..').PolygonOperationResult;
@@ -26,6 +26,7 @@ declare const meta: {
26
26
  onIsImageJunk?: (isJunk: boolean) => void;
27
27
  onNotification?: (notification: import('../..').SIANotification) => void;
28
28
  onSelectAnnotation?: (annotation: import('../../models').Annotation) => void;
29
+ onTimeTravel?: (timeTravelAction: import('../..').TimeTravelChanges) => void;
29
30
  }) => import("react/jsx-runtime").JSX.Element;
30
31
  parameters: {
31
32
  layout: string;
@@ -56,6 +57,7 @@ declare const meta: {
56
57
  onIsImageJunk?: (isJunk: boolean) => void;
57
58
  onNotification?: (notification: import('../..').SIANotification) => void;
58
59
  onSelectAnnotation?: (annotation: import('../../models').Annotation) => void;
60
+ onTimeTravel?: (timeTravelAction: import('../..').TimeTravelChanges) => void;
59
61
  }>) => import("react/jsx-runtime").JSX.Element)[];
60
62
  };
61
63
  export default meta;
package/dist/types.d.ts CHANGED
@@ -55,3 +55,8 @@ export type Vector2 = {
55
55
  x: number;
56
56
  y: number;
57
57
  };
58
+ export type TimeTravelChanges = {
59
+ addedAnnotations: Annotation[];
60
+ removedAnnotations: Annotation[];
61
+ changedAnnotations: Annotation[];
62
+ };
@@ -1 +1 @@
1
- var c=Object.defineProperty;var s=(i,t,r)=>t in i?c(i,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):i[t]=r;var A=(i,t,r)=>s(i,typeof t!="symbol"?t+"":t,r);import e from"../models/KeyAction.js";class o{constructor(t=void 0){A(this,"isControlDown",!1);A(this,"keyActionHandler");this.keyActionHandler=t}keyDown(t,r=!1,a=!1){switch(t){case"Enter":this.triggerKeyAction(e.EDIT_LABEL);break;case"Delete":this.triggerKeyAction(e.DELETE_ANNO);break;case"Backspace":this.triggerKeyAction(e.DELETE_ANNO);break;case"z":a&&this.triggerKeyAction(e.UNDO);break;case"y":a&&this.triggerKeyAction(e.REDO);break;case"Tab":r?this.triggerKeyAction(e.TRAVERSE_ANNOS_BACKWARDS):this.triggerKeyAction(e.TRAVERSE_ANNOS);break;case"w":this.triggerKeyAction(e.CAM_MOVE_UP);break;case"s":this.triggerKeyAction(e.CAM_MOVE_DOWN);break;case"a":this.triggerKeyAction(e.CAM_MOVE_LEFT);break;case"d":this.triggerKeyAction(e.CAM_MOVE_RIGHT);break;case"e":this.triggerKeyAction(e.RECREATE_ANNO);break;case"j":this.triggerKeyAction(e.TOGGLE_IMAGE_JUNK);break;case"c":a?this.triggerKeyAction(e.COPY_ANNOTATION):this.triggerKeyAction(e.TOGGLE_ANNO_COMMENT_INPUT);break;case"v":a&&this.triggerKeyAction(e.PASTE_ANNOTATION);break;case"Escape":this.triggerKeyAction(e.DELETE_ANNO_IN_CREATION);break;default:return!1}return!0}triggerKeyAction(t){this.keyActionHandler&&this.keyActionHandler(t)}}export{o as default};
1
+ import e from"../models/KeyAction.js";class c{isControlDown=!1;keyActionHandler;constructor(t=void 0){this.keyActionHandler=t}keyDown(t,r=!1,i=!1){switch(t){case"Enter":this.triggerKeyAction(e.EDIT_LABEL);break;case"Delete":this.triggerKeyAction(e.DELETE_ANNO);break;case"Backspace":this.triggerKeyAction(e.DELETE_ANNO);break;case"z":i&&this.triggerKeyAction(e.UNDO);break;case"y":i&&this.triggerKeyAction(e.REDO);break;case"Tab":r?this.triggerKeyAction(e.TRAVERSE_ANNOS_BACKWARDS):this.triggerKeyAction(e.TRAVERSE_ANNOS);break;case"w":this.triggerKeyAction(e.CAM_MOVE_UP);break;case"s":this.triggerKeyAction(e.CAM_MOVE_DOWN);break;case"a":this.triggerKeyAction(e.CAM_MOVE_LEFT);break;case"d":this.triggerKeyAction(e.CAM_MOVE_RIGHT);break;case"e":this.triggerKeyAction(e.RECREATE_ANNO);break;case"j":this.triggerKeyAction(e.TOGGLE_IMAGE_JUNK);break;case"c":i?this.triggerKeyAction(e.COPY_ANNOTATION):this.triggerKeyAction(e.TOGGLE_ANNO_COMMENT_INPUT);break;case"v":i&&this.triggerKeyAction(e.PASTE_ANNOTATION);break;case"Escape":this.triggerKeyAction(e.DELETE_ANNO_IN_CREATION);break;default:return!1}return!0}triggerKeyAction(t){this.keyActionHandler&&this.keyActionHandler(t)}}export{c as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lost-sia",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Single Image Annotation Tool",
5
5
  "license": "MIT",
6
6
  "repository": "l3p-cv/lost-sia",
@@ -57,36 +57,34 @@
57
57
  "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
58
58
  },
59
59
  "dependencies": {
60
- "@fortawesome/free-regular-svg-icons": "^6.7.2",
61
- "@fortawesome/free-solid-svg-icons": "^6.7.2",
62
- "@fortawesome/react-fontawesome": "^0.2.0",
63
- "react": "^18.0.0",
64
- "react-dom": "^18.0.0",
65
- "react-draggable": "^4.4.6",
66
- "sass": "^1.89.2",
60
+ "@fortawesome/free-regular-svg-icons": "^7.1.0",
61
+ "@fortawesome/free-solid-svg-icons": "^7.1.0",
62
+ "@fortawesome/react-fontawesome": "^3.1.1",
63
+ "react": "^19.2.1",
64
+ "react-dom": "^19.2.1",
65
+ "react-draggable": "^4.5.0",
66
+ "sass": "^1.95.0",
67
67
  "semantic-ui-css": "2.5.0",
68
68
  "semantic-ui-react": "^2.0.3"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@coreui/react": "^5.9.1",
72
72
  "prop-types": "^15.5.4",
73
- "react": "^18.0.0",
74
- "react-dom": "^18.0.0"
73
+ "react": "^19.2.1",
74
+ "react-dom": "^19.2.1"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@eslint/eslintrc": "^3.3.1",
78
78
  "@eslint/js": "^9.39.1",
79
79
  "@microsoft/api-extractor": "^7.52.13",
80
- "@storybook/addon-docs": "^9.1.10",
81
- "@storybook/addon-links": "^9.0.12",
82
- "@storybook/react": "^9.0.12",
83
- "@storybook/react-vite": "^9.0.12",
84
- "@storybook/test": "9.0.0-alpha.2",
85
- "@types/react": "^19.1.16",
80
+ "@storybook/addon-docs": "^10.1.6",
81
+ "@storybook/addon-links": "^10.1.6",
82
+ "@storybook/react": "^10.1.6",
83
+ "@storybook/react-vite": "^10.1.6",
84
+ "@types/react": "^19.2.7",
86
85
  "@typescript-eslint/eslint-plugin": "^8.46.1",
87
86
  "@typescript-eslint/parser": "^8.46.1",
88
- "@vitejs/plugin-react": "^4.3.0",
89
- "cross-env": "^7.0.3",
87
+ "@vitejs/plugin-react": "^5.1.2",
90
88
  "eslint": "^9.39.1",
91
89
  "eslint-config-standard": "^17.1.0",
92
90
  "eslint-config-standard-react": "^13.0.0",
@@ -96,15 +94,15 @@
96
94
  "eslint-plugin-react": "^7.37.5",
97
95
  "eslint-plugin-standard": "^5.0.0",
98
96
  "eslint-plugin-storybook": "^10.0.7",
99
- "glob": "^11.0.3",
97
+ "glob": "^13.0.0",
100
98
  "lodash-es": "^4.17.21",
101
99
  "prettier": "^3.3.3",
102
100
  "react-redux": "^9.1.2",
103
101
  "redux": "^5.0.1",
104
- "storybook": "^9.0.12",
102
+ "storybook": "^10.1.6",
105
103
  "unplugin-dts": "^1.0.0-beta.6",
106
- "vite": "^5.4.19",
107
- "vitest": "^1.6.1"
104
+ "vite": "^7.2.7",
105
+ "vitest": "^4.0.15"
108
106
  },
109
107
  "eslintConfig": {
110
108
  "extends": [
@@ -48,7 +48,9 @@ const AnnoBar = ({
48
48
  useEffect(() => {
49
49
  // get the most top left point from the annotation
50
50
  const topPoints: Point[] = transform.getTopPoint(annotationCoordinates)
51
- const newTopLeftPoint: Point = transform.getMostLeftPoints(topPoints)[0]
51
+ const newTopLeftPoints: Point[] = transform.getMostLeftPoints(topPoints)
52
+ const newTopLeftPoint: Point =
53
+ newTopLeftPoints.length > 0 ? newTopLeftPoints[0] : { x: 0, y: 0 }
52
54
 
53
55
  setTopLeftPoint(newTopLeftPoint)
54
56
 
@@ -63,6 +63,7 @@ type CanvasProps = {
63
63
  onSetIsImageJunk: (newJunkState: boolean) => void
64
64
  onSetSelectedTool: (tool: AnnotationTool) => void
65
65
  onShouldDeleteAnno: (internalAnnoId: number) => void
66
+ onTraverseAnnotationHistory: (isUndo: boolean) => void
66
67
  }
67
68
 
68
69
  const Canvas = ({
@@ -92,6 +93,7 @@ const Canvas = ({
92
93
  onSetIsImageJunk,
93
94
  onSetSelectedTool = (_) => {},
94
95
  onShouldDeleteAnno,
96
+ onTraverseAnnotationHistory,
95
97
  }: CanvasProps) => {
96
98
  const [editorMode, setEditorMode] = useState<EditorModes>(EditorModes.VIEW)
97
99
  const [annoTimestamp, setAnnoTimestamp] = useState<number | undefined>()
@@ -360,10 +362,10 @@ const Canvas = ({
360
362
  console.log('KeyAction TODO: LEAVE_ANNO_ADD_MODE')
361
363
  break
362
364
  case KeyAction.UNDO:
363
- console.log('KeyAction TODO: UNDO')
365
+ onTraverseAnnotationHistory(true)
364
366
  break
365
367
  case KeyAction.REDO:
366
- console.log('KeyAction TODO: REDO')
368
+ onTraverseAnnotationHistory(false)
367
369
  break
368
370
  case KeyAction.TRAVERSE_ANNOS:
369
371
  traverseAnnos()
@@ -67,7 +67,7 @@ const LabelInput = ({
67
67
  </CPopover>
68
68
 
69
69
  <CDropdown visible={isVisible} autoClose={false} style={{ marginTop: -25 }}>
70
- {/* this invisible toggle has to be here, othervise the menu is not showing as intended */}
70
+ {/* this invisible toggle has to be here, otherwise the menu is not showing as intended */}
71
71
  <CDropdownToggle style={{ display: 'none' }} />
72
72
  <CDropdownMenu>
73
73
  <div className="px-3 py-2">
package/src/Sia.tsx CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  Label,
14
14
  PolygonOperationResult,
15
15
  SIANotification,
16
+ TimeTravelChanges,
16
17
  UiConfig,
17
18
  } from './types'
18
19
 
@@ -39,12 +40,13 @@ type SiaProps = {
39
40
  onIsImageJunk?: (isJunk: boolean) => void
40
41
  onNotification?: (notification: SIANotification) => void
41
42
  onSelectAnnotation?: (annotation: Annotation) => void
43
+ onTimeTravel?: (timeTravelAction: TimeTravelChanges) => void
42
44
  }
43
45
 
44
46
  /**
45
47
  * Main SIA component
46
48
  */
47
- const Sia2 = ({
49
+ const Sia = ({
48
50
  additionalButtons,
49
51
  allowedTools: propAllowedTools,
50
52
  polygonOperationResult = { annotationsToDelete: [], polygonsToCreate: [] },
@@ -67,6 +69,7 @@ const Sia2 = ({
67
69
  onIsImageJunk = () => {},
68
70
  onNotification = (_) => {},
69
71
  onSelectAnnotation = (_) => {},
72
+ onTimeTravel = (_) => {},
70
73
  }: SiaProps) => {
71
74
  const marginBetweenToolbarAndContainerPixels: number = 10
72
75
 
@@ -78,6 +81,12 @@ const Sia2 = ({
78
81
  const [annotations, setAnnotations] = useState<Annotation[]>([])
79
82
  const [annotationSettings, setAnnotationSettings] = useState<AnnotationSettings>()
80
83
 
84
+ // tracks how far we went back in the history
85
+ const [annotationHistoryIndex, setAnnotationHistoryIndex] = useState<
86
+ number | undefined
87
+ >()
88
+ const [annotationHistory, setAnnotationHistory] = useState<Annotation[][]>([])
89
+
81
90
  const [uiConfig, setUiConfig] = useState<UiConfig>()
82
91
 
83
92
  const [selectedAnnotation, setSelectedAnnotation] = useState<Annotation>()
@@ -86,6 +95,26 @@ const Sia2 = ({
86
95
  defaultAnnotationTool ?? AnnotationTool.Point,
87
96
  )
88
97
 
98
+ const updateAnnotationHistory = (annotations: Annotation[]) => {
99
+ const _annotations = [...annotations]
100
+ const _annotationHistory = [...annotationHistory]
101
+
102
+ // user did some changes from within the past
103
+ // time to create an alternative timeline and delete the original one
104
+ if (annotationHistoryIndex !== undefined) {
105
+ // remove everything after the state the user is
106
+ _annotationHistory.splice(annotationHistoryIndex + 1)
107
+ }
108
+
109
+ // update the list with out latest change (it is always living in the present)
110
+ _annotationHistory.push(_annotations)
111
+
112
+ // keep history index marker in the present
113
+ setAnnotationHistoryIndex(undefined)
114
+
115
+ setAnnotationHistory(_annotationHistory)
116
+ }
117
+
89
118
  // for adjusting the container/canvas size
90
119
  // const [toolbarHeight, setToolbarHeight] = useState<number>(-1);
91
120
 
@@ -121,6 +150,7 @@ const Sia2 = ({
121
150
 
122
151
  setAnnotations(_annotations)
123
152
  setSelectedAnnotation(undefined)
153
+ updateAnnotationHistory(_annotations)
124
154
 
125
155
  // inform the outside world about our changes
126
156
  onAnnoDeleted(removedAnno, _annotations)
@@ -159,6 +189,7 @@ const Sia2 = ({
159
189
  setUsedInternalIds([...new Array(internalAnnoId).keys()])
160
190
 
161
191
  setAnnotations(_annotations)
192
+ updateAnnotationHistory(_annotations)
162
193
  }
163
194
 
164
195
  const createNewInternalAnnotationId = (): number => {
@@ -206,11 +237,86 @@ const Sia2 = ({
206
237
  onIsImageJunk(newJunkState)
207
238
  }
208
239
 
240
+ const handleTraverseAnnotationHistory = (isUndo: boolean) => {
241
+ // undefined -> last element
242
+ const _annotationHistoryIndex = annotationHistoryIndex ?? annotationHistory.length - 1
243
+
244
+ const isPresent = _annotationHistoryIndex == annotationHistory.length - 1
245
+ const isFirst = _annotationHistoryIndex == 0
246
+
247
+ // we cannot go into the future (yet) or past the past
248
+ if ((isPresent && !isUndo) || (isFirst && isUndo)) return
249
+
250
+ // request time travel using state update
251
+ const newHistoryIndex = _annotationHistoryIndex + (isUndo ? -1 : 1)
252
+ setAnnotationHistoryIndex(newHistoryIndex)
253
+ }
254
+
255
+ const getTimeTravelInductedChanges = (
256
+ timeTravelledAnnotations: Annotation[],
257
+ ): TimeTravelChanges => {
258
+ const addedAnnotations: Annotation[] = []
259
+ const removedAnnotations: Annotation[] = []
260
+ const changedAnnotations: Annotation[] = []
261
+
262
+ // check which items have been added or changed
263
+ for (const travelledAnno of timeTravelledAnnotations) {
264
+ const currentAnno = annotations.find(
265
+ (anno) => anno.internalId === travelledAnno.internalId,
266
+ )
267
+
268
+ if (!currentAnno) {
269
+ addedAnnotations.push(travelledAnno)
270
+ } else if (JSON.stringify(currentAnno) !== JSON.stringify(travelledAnno)) {
271
+ changedAnnotations.push(travelledAnno)
272
+ }
273
+ }
274
+
275
+ // check which items have been deleted
276
+ for (const anno of annotations) {
277
+ const travelledAnno = timeTravelledAnnotations.find(
278
+ (tAnno) => tAnno.internalId === anno.internalId,
279
+ )
280
+
281
+ if (!travelledAnno) {
282
+ removedAnnotations.push(anno)
283
+ }
284
+ }
285
+
286
+ return { addedAnnotations, removedAnnotations, changedAnnotations }
287
+ }
288
+
289
+ useEffect(() => {
290
+ if (
291
+ annotationHistoryIndex == undefined ||
292
+ annotationHistoryIndex < 0 ||
293
+ annotationHistoryIndex > annotationHistory.length - 1
294
+ )
295
+ return
296
+
297
+ // update the shown annotations when we travel in time using the history index
298
+ const refSelectedAnnotationsFromHistory = annotationHistory[annotationHistoryIndex]
299
+ const selectedAnnotationsFromHistory: Annotation[] = [
300
+ ...refSelectedAnnotationsFromHistory,
301
+ ]
302
+
303
+ setAnnotations(selectedAnnotationsFromHistory)
304
+
305
+ const timeTravelInductedChanges: TimeTravelChanges = getTimeTravelInductedChanges(
306
+ selectedAnnotationsFromHistory,
307
+ )
308
+ onTimeTravel(timeTravelInductedChanges)
309
+ }, [annotationHistoryIndex])
310
+
209
311
  useEffect(() => {
210
312
  // remove current annotations when the image changes
211
313
  if (image === undefined) {
212
314
  setAnnotations([])
213
315
  setSelectedAnnotation(undefined)
316
+
317
+ // reset time machine
318
+ setAnnotationHistory([])
319
+ setAnnotationHistoryIndex(undefined)
214
320
  }
215
321
  }, [image])
216
322
 
@@ -397,6 +503,7 @@ const Sia2 = ({
397
503
  setAnnotations(_annotations)
398
504
  setSelectedAnnotation(annotation)
399
505
  onAnnoCreated(annotation, _annotations)
506
+ // dont update history here - we dont have a finished anno at this point
400
507
  }}
401
508
  onAnnoChanged={(changedAnno: Annotation) => {
402
509
  // update annotation list
@@ -411,6 +518,11 @@ const Sia2 = ({
411
518
  _annotations[annoListIndex] = changedAnno
412
519
  setAnnotations(_annotations)
413
520
 
521
+ // only update history for full/finished annotations
522
+ if (changedAnno.status !== AnnotationStatus.CREATING) {
523
+ updateAnnotationHistory(_annotations)
524
+ }
525
+
414
526
  // inform the outside world about our change
415
527
  onAnnoChanged(changedAnno, _annotations)
416
528
  }}
@@ -457,6 +569,7 @@ const Sia2 = ({
457
569
  }
458
570
 
459
571
  setAnnotations(_annotations)
572
+ updateAnnotationHistory(_annotations)
460
573
 
461
574
  // mark annotation as fully created
462
575
  changedAnno.status = AnnotationStatus.CREATED
@@ -474,6 +587,7 @@ const Sia2 = ({
474
587
  }}
475
588
  onSetSelectedTool={setSelectedAnnoTool}
476
589
  onShouldDeleteAnno={deleteAnnotationByInternalId}
590
+ onTraverseAnnotationHistory={handleTraverseAnnotationHistory}
477
591
  />
478
592
  )}
479
593
  </div>
@@ -481,4 +595,4 @@ const Sia2 = ({
481
595
  )
482
596
  }
483
597
 
484
- export default Sia2
598
+ export default Sia
package/src/types.ts CHANGED
@@ -65,3 +65,9 @@ export type Vector2 = {
65
65
  x: number
66
66
  y: number
67
67
  }
68
+
69
+ export type TimeTravelChanges = {
70
+ addedAnnotations: Annotation[]
71
+ removedAnnotations: Annotation[]
72
+ changedAnnotations: Annotation[]
73
+ }