lost-sia 3.0.0 → 3.1.1-alpha3

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.1-alpha3",
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>()
@@ -335,10 +337,28 @@ const Canvas = ({
335
337
  onSelectAnnotation(annotationToPaste)
336
338
  }
337
339
 
340
+ /** Returns the page-space position of an annotation's top-left corner.
341
+ * @param stageCoords - annotation coordinates already in stage (pixel) space
342
+ */
343
+ const getAnnoTopLeftPagePosition = (stageCoords: Point[]): Point => {
344
+ const leftPoints: Point[] = transform.getMostLeftPoints(stageCoords)
345
+ const topLeftPoint: Point = transform.getTopPoint(leftPoints)[0]
346
+ return transform.convertStageToPage(topLeftPoint, pageToStageOffset, svgScale, svgTranslation)
347
+ }
348
+
338
349
  const handleKeyAction = (keyAction: KeyAction) => {
339
350
  switch (keyAction) {
340
351
  case KeyAction.EDIT_LABEL:
341
- if (selectedAnnotation) setIsLabelInputVisible(true)
352
+ if (selectedAnnotation) {
353
+ // selectedAnnotation coordinates are in the percentaged system — convert to stage first
354
+ const stageCoords = transform.convertPercentagedCoordinatesToStage(
355
+ selectedAnnotation.coordinates,
356
+ imgSize,
357
+ stageSize,
358
+ )
359
+ setLabelInputPosition(getAnnoTopLeftPagePosition(stageCoords))
360
+ setIsLabelInputVisible(true)
361
+ }
342
362
  break
343
363
  case KeyAction.DELETE_ANNO:
344
364
  if (selectedAnnotation) onShouldDeleteAnno(selectedAnnotation.internalId)
@@ -360,10 +380,10 @@ const Canvas = ({
360
380
  console.log('KeyAction TODO: LEAVE_ANNO_ADD_MODE')
361
381
  break
362
382
  case KeyAction.UNDO:
363
- console.log('KeyAction TODO: UNDO')
383
+ onTraverseAnnotationHistory(true)
364
384
  break
365
385
  case KeyAction.REDO:
366
- console.log('KeyAction TODO: REDO')
386
+ onTraverseAnnotationHistory(false)
367
387
  break
368
388
  case KeyAction.TRAVERSE_ANNOS:
369
389
  traverseAnnos()
@@ -741,16 +761,7 @@ const Canvas = ({
741
761
  onSelectAnnotation(percentagedAnnotation)
742
762
 
743
763
  // get top left point of annotation
744
- const leftPoints: Point[] = transform.getMostLeftPoints(annotation.coordinates)
745
- const topLeftPoint: Point = transform.getTopPoint(leftPoints)[0]
746
- const pageTopLeftPoint: Point = transform.convertStageToPage(
747
- topLeftPoint,
748
- pageToStageOffset,
749
- svgScale,
750
- svgTranslation,
751
- )
752
-
753
- setLabelInputPosition(pageTopLeftPoint)
764
+ setLabelInputPosition(getAnnoTopLeftPagePosition(annotation.coordinates))
754
765
  }
755
766
 
756
767
  const handleOnAnnoChanged = (annotation: Annotation) => {
@@ -919,22 +930,21 @@ const Canvas = ({
919
930
  }
920
931
  }
921
932
 
933
+ // Look up the current annotation from scaledAnnotations
934
+ const currentScaled = scaledAnnotations.find(
935
+ (a) => a.internalId === selectedAnnotation.internalId,
936
+ )
937
+ if (!currentScaled) return
938
+
922
939
  // change the status to CHANGED when the annotation was loaded (initialAnnotations)
923
940
  const newAnnotationStatus: AnnotationStatus =
924
- selectedAnnotation.status === AnnotationStatus.LOADED
941
+ currentScaled.status === AnnotationStatus.LOADED
925
942
  ? AnnotationStatus.CHANGED
926
- : selectedAnnotation.status
943
+ : currentScaled.status
927
944
 
928
- // selectedAnnotation comes from SIA and is therefore in the percentaged system
929
- // convert it first
930
- // also update the new labels
931
945
  const updatedAnno: Annotation = {
932
946
  ...selectedAnnotation,
933
- coordinates: transform.convertPercentagedCoordinatesToStage(
934
- selectedAnnotation.coordinates,
935
- imgSize,
936
- stageSize,
937
- ),
947
+ coordinates: currentScaled.coordinates,
938
948
  labelIds: [...selectedLabelIds],
939
949
  status: newAnnotationStatus,
940
950
  }
@@ -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
  }}
@@ -445,6 +557,8 @@ const Sia2 = ({
445
557
  }
446
558
  }
447
559
  }
560
+ // mark annotation as fully created before storing it
561
+ changedAnno.status = AnnotationStatus.CREATED
448
562
 
449
563
  // are we just marking an existing annotation as finished or did we created it in the same frame
450
564
  if (hasAnnoJustBeenCreated) _annotations.push(changedAnno)
@@ -457,9 +571,7 @@ const Sia2 = ({
457
571
  }
458
572
 
459
573
  setAnnotations(_annotations)
460
-
461
- // mark annotation as fully created
462
- changedAnno.status = AnnotationStatus.CREATED
574
+ updateAnnotationHistory(_annotations)
463
575
 
464
576
  // inform the outer world about our changes
465
577
  onAnnoCreationFinished(changedAnno, _annotations)
@@ -474,6 +586,7 @@ const Sia2 = ({
474
586
  }}
475
587
  onSetSelectedTool={setSelectedAnnoTool}
476
588
  onShouldDeleteAnno={deleteAnnotationByInternalId}
589
+ onTraverseAnnotationHistory={handleTraverseAnnotationHistory}
477
590
  />
478
591
  )}
479
592
  </div>
@@ -481,4 +594,4 @@ const Sia2 = ({
481
594
  )
482
595
  }
483
596
 
484
- export default Sia2
597
+ 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
+ }