@annotorious/annotorious 3.7.21 → 3.8.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.
@@ -7,5 +7,6 @@ export interface RectangleGeometry extends Geometry {
7
7
  y: number;
8
8
  w: number;
9
9
  h: number;
10
+ rot?: number;
10
11
  bounds: Bounds;
11
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.7.21",
3
+ "version": "3.8.0",
4
4
  "description": "Add image annotation functionality to any web page with a few lines of JavaScript",
5
5
  "author": "Rainer Simon",
6
6
  "license": "BSD-3-Clause",
@@ -55,7 +55,7 @@
55
55
  "vitest": "^3.2.4"
56
56
  },
57
57
  "dependencies": {
58
- "@annotorious/core": "3.7.21",
58
+ "@annotorious/core": "3.8.0",
59
59
  "dequal": "^2.0.3",
60
60
  "rbush": "^4.0.1",
61
61
  "simplify-js": "^1.2.4",
@@ -93,6 +93,7 @@
93
93
  fill: transparent;
94
94
  stroke: transparent;
95
95
  stroke-width: 6px;
96
+ vector-effect: non-scaling-stroke;
96
97
  }
97
98
 
98
99
  .a9s-shape-handle {
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" generics="I extends Annotation, E extends unknown">
2
- import { type SvelteComponent, onMount } from 'svelte';
2
+ import { onMount } from 'svelte';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
- import type { Annotation, DrawingStyleExpression, StoreChangeEvent, User } from '@annotorious/core';
4
+ import type { Annotation, DrawingStyleExpression, Selection, StoreChangeEvent, User } from '@annotorious/core';
5
5
  import { isImageAnnotation, ShapeType } from '../model';
6
6
  import type { ImageAnnotation, Shape} from '../model';
7
7
  import { getEditor, EditorMount } from './editors';
@@ -56,7 +56,7 @@
56
56
 
57
57
  let editableAnnotations: ImageAnnotation[] | undefined;
58
58
 
59
- $: trackSelection($selection.selected);
59
+ $: trackSelection(($selection as Selection).selected);
60
60
 
61
61
  const trackSelection = (selected: { id: string, editable?: boolean }[]) => {
62
62
  if (storeObserver)
@@ -1,9 +1,17 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
2
3
  import Handle from '../Handle.svelte';
3
4
  import { getMaskDimensions } from '../../utils';
4
- import type { Rectangle, Shape } from '../../../model';
5
+ import { boundsFromPoints, type Rectangle, type RectangleGeometry, type Shape } from '../../../model';
5
6
  import type { Transform } from '../../Transform';
6
7
  import { Editor } from '..';
8
+ import {
9
+ getRotatedCorners,
10
+ getRotationHandlePosition,
11
+ transformDeltaToLocalCoords,
12
+ angleFromPoints,
13
+ snapAngle
14
+ } from './rotationUtils';
7
15
 
8
16
  /** Props */
9
17
  export let shape: Rectangle;
@@ -12,75 +20,134 @@
12
20
  export let viewportScale: number = 1;
13
21
  export let svgEl: SVGSVGElement;
14
22
 
23
+ let shiftPressed = false;
24
+
25
+ $: ROTATION_HANDLE_OFFSET = 20 / viewportScale;
26
+
15
27
  $: geom = shape.geometry;
28
+ $: rotatedCorners = getRotatedCorners(geom.x, geom.y, geom.w, geom.h, geom.rot);
29
+ $: rotationHandlePos = getRotationHandlePosition(geom, ROTATION_HANDLE_OFFSET);
16
30
 
17
31
  const editor = (rectangle: Shape, handle: string, delta: [number, number]) => {
18
- const initialBounds = rectangle.geometry.bounds;
19
-
20
- let [x0, y0] = [initialBounds.minX, initialBounds.minY];
21
- let [x1, y1] = [initialBounds.maxX, initialBounds.maxY];
32
+ let { x, y, w, h, rot = 0 } = (rectangle.geometry as RectangleGeometry);
22
33
 
23
34
  const [dx, dy] = delta;
24
35
 
25
- if (handle === 'SHAPE') {
26
- x0 += dx;
27
- x1 += dx;
28
- y0 += dy;
29
- y1 += dy;
36
+ if (handle === 'ROTATION') {
37
+ const handlePos = getRotationHandlePosition(rectangle.geometry as RectangleGeometry, ROTATION_HANDLE_OFFSET);
38
+
39
+ // Handle position after moving by delta
40
+ const currentHandleX = handlePos[0] + dx;
41
+ const currentHandleY = handlePos[1] + dy;
42
+
43
+ // Calculate the new rotation angle
44
+ const center: [number, number] = [x + w / 2, y + h / 2];
45
+ rot += angleFromPoints([handlePos[0], handlePos[1]], [currentHandleX, currentHandleY], center);
46
+
47
+ // Snap to 10 degrees if SHIFT is held
48
+ if (shiftPressed)
49
+ rot = snapAngle(rot);
50
+ } else if (handle === 'SHAPE') {
51
+ // Moving the entire shape - translate it without rotation change
52
+ x += dx;
53
+ y += dy;
30
54
  } else {
55
+ // Edge or corner handle - resize in local (rotated) coordinate space
56
+ let localX0 = 0;
57
+ let localY0 = 0;
58
+ let localX1 = w;
59
+ let localY1 = h;
60
+
61
+ const [localDx, localDy] = rot !== 0
62
+ ? transformDeltaToLocalCoords(dx, dy, rot)
63
+ : [dx, dy];
64
+
31
65
  switch (handle) {
32
66
  case 'TOP':
33
67
  case 'TOP_LEFT':
34
- case 'TOP_RIGHT': {
35
- y0 += dy;
68
+ case 'TOP_RIGHT':
69
+ localY0 += localDy;
36
70
  break;
37
- }
38
-
39
71
  case 'BOTTOM':
40
72
  case 'BOTTOM_LEFT':
41
- case 'BOTTOM_RIGHT': {
42
- y1 += dy;
73
+ case 'BOTTOM_RIGHT':
74
+ localY1 += localDy;
43
75
  break;
44
- }
45
76
  }
46
77
 
47
78
  switch (handle) {
48
79
  case 'LEFT':
49
80
  case 'TOP_LEFT':
50
- case 'BOTTOM_LEFT': {
51
- x0 += dx;
81
+ case 'BOTTOM_LEFT':
82
+ localX0 += localDx;
52
83
  break;
53
- }
54
-
55
84
  case 'RIGHT':
56
85
  case 'TOP_RIGHT':
57
- case 'BOTTOM_RIGHT': {
58
- x1 += dx;
86
+ case 'BOTTOM_RIGHT':
87
+ localX1 += localDx;
59
88
  break;
60
- }
61
89
  }
90
+
91
+ // The center shifts as edges move - calculate new center in local space
92
+ const newLocalCx = (localX0 + localX1) / 2;
93
+ const newLocalCy = (localY0 + localY1) / 2;
94
+
95
+ w = Math.abs(localX1 - localX0);
96
+ h = Math.abs(localY1 - localY0);
97
+
98
+ // Rotate the local center offset back to world space
99
+ const oldCenter: [number, number] = [
100
+ x + (rectangle.geometry as RectangleGeometry).w / 2,
101
+ y + (rectangle.geometry as RectangleGeometry).h / 2
102
+ ];
103
+
104
+ const localCenterOffset: [number, number] = [
105
+ newLocalCx - (rectangle.geometry as RectangleGeometry).w / 2,
106
+ newLocalCy - (rectangle.geometry as RectangleGeometry).h / 2
107
+ ];
108
+
109
+ const cos = Math.cos(rot);
110
+ const sin = Math.sin(rot);
111
+
112
+ const worldCx = oldCenter[0] + localCenterOffset[0] * cos - localCenterOffset[1] * sin;
113
+ const worldCy = oldCenter[1] + localCenterOffset[0] * sin + localCenterOffset[1] * cos;
114
+
115
+ x = worldCx - w / 2;
116
+ y = worldCy - h / 2;
62
117
  }
63
118
 
64
- const x = Math.min(x0, x1);
65
- const y = Math.min(y0, y1);
66
- const w = Math.abs(x1 - x0);
67
- const h = Math.abs(y1 - y0);
119
+ // Calculate new bounds
120
+ const bounds = boundsFromPoints(rotatedCorners);
68
121
 
69
122
  return {
70
123
  ...rectangle,
71
124
  geometry: {
72
- x, y, w, h,
73
- bounds: {
74
- minX: x,
75
- minY: y,
76
- maxX: x + w,
77
- maxY: y + h
78
- }
125
+ x, y, w, h, rot,
126
+ bounds
79
127
  }
80
128
  };
81
129
  }
82
130
 
83
- $: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
131
+ onMount(() => {
132
+ // Track SHIFT key
133
+ const onKeyDown = (evt: KeyboardEvent) => {
134
+ if (evt.key === 'Shift') shiftPressed = true;
135
+ }
136
+
137
+ const onKeyUp = (evt: KeyboardEvent) => {
138
+ if (evt.key === 'Shift') shiftPressed = false;
139
+ }
140
+
141
+ window.addEventListener('keydown', onKeyDown);
142
+ window.addEventListener('keyup', onKeyUp);
143
+
144
+ return () => {
145
+ window.removeEventListener('keydown', onKeyDown);
146
+ window.removeEventListener('keyup', onKeyUp);
147
+ };
148
+ });
149
+
150
+ $: mask = getMaskDimensions(geom.bounds, 5 / viewportScale);
84
151
 
85
152
  const maskId = `rect-mask-${Math.random().toString(36).substring(2, 12)}`;
86
153
  </script>
@@ -98,73 +165,122 @@
98
165
  <defs>
99
166
  <mask id={maskId} class="a9s-rectangle-editor-mask">
100
167
  <rect class="rect-mask-bg" x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
101
- <rect class="rect-mask-fg" x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
168
+ <polygon
169
+ class="rect-mask-fg"
170
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
102
171
  </mask>
103
172
  </defs>
104
173
 
105
- <rect
106
- class="a9s-outer"
107
- mask={`url(#${maskId})`}
108
- on:pointerdown={grab('SHAPE')}
109
- x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
174
+ <!-- Rotation handle -->
175
+ <g>
176
+ <line
177
+ class="a9s-rotation-handle-line-bg"
178
+ x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
179
+ y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
180
+ x2={rotationHandlePos[0]}
181
+ y2={rotationHandlePos[1]}
182
+ pointer-events="none" />
110
183
 
111
- <rect
112
- class="a9s-inner a9s-shape-handle"
113
- style={computedStyle}
114
- on:pointerdown={grab('SHAPE')}
115
- x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
184
+ <line
185
+ class="a9s-rotation-handle-line-fg"
186
+ x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
187
+ y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
188
+ x2={rotationHandlePos[0]}
189
+ y2={rotationHandlePos[1]}
190
+ pointer-events="none" />
116
191
 
117
- <rect
118
- class="a9s-edge-handle a9s-edge-handle-top"
119
- on:pointerdown={grab('TOP')}
120
- x={geom.x} y={geom.y} height={1} width={geom.w} />
192
+ <Handle
193
+ class="a9s-rotation-handle"
194
+ on:pointerdown={grab('ROTATION')}
195
+ x={rotationHandlePos[0]} y={rotationHandlePos[1]}
196
+ scale={viewportScale} />
197
+ </g>
121
198
 
122
- <rect
199
+ <!-- Rectangle shape -->
200
+ <g>
201
+ <polygon
202
+ class="a9s-outer"
203
+ mask={`url(#${maskId})`}
204
+ on:pointerdown={grab('SHAPE')}
205
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
206
+
207
+ <polygon
208
+ class="a9s-inner a9s-shape-handle"
209
+ style={computedStyle}
210
+ on:pointerdown={grab('SHAPE')}
211
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
212
+ </g>
213
+
214
+ <!-- Edge handles -->
215
+ <line
216
+ class="a9s-edge-handle a9s-edge-handle-top"
217
+ x1={rotatedCorners[0][0]} y1={rotatedCorners[0][1]}
218
+ x2={rotatedCorners[1][0]} y2={rotatedCorners[1][1]}
219
+ on:pointerdown={grab('TOP')} />
220
+
221
+ <line
123
222
  class="a9s-edge-handle a9s-edge-handle-right"
124
- on:pointerdown={grab('RIGHT')}
125
- x={geom.x + geom.w} y={geom.y} height={geom.h} width={1}/>
223
+ x1={rotatedCorners[1][0]} y1={rotatedCorners[1][1]}
224
+ x2={rotatedCorners[2][0]} y2={rotatedCorners[2][1]}
225
+ on:pointerdown={grab('RIGHT')} />
126
226
 
127
- <rect
128
- class="a9s-edge-handle a9s-edge-handle-bottom"
129
- on:pointerdown={grab('BOTTOM')}
130
- x={geom.x} y={geom.y + geom.h} height={1} width={geom.w} />
227
+ <line
228
+ class="a9s-edge-handle a9s-edge-handle-bottom"
229
+ x1={rotatedCorners[2][0]} y1={rotatedCorners[2][1]}
230
+ x2={rotatedCorners[3][0]} y2={rotatedCorners[3][1]}
231
+ on:pointerdown={grab('BOTTOM')} />
131
232
 
132
- <rect
133
- class="a9s-edge-handle a9s-edge-handle-left"
134
- on:pointerdown={grab('LEFT')}
135
- x={geom.x} y={geom.y} height={geom.h} width={1} />
233
+ <line
234
+ class="a9s-edge-handle a9s-edge-handle-left"
235
+ x1={rotatedCorners[3][0]} y1={rotatedCorners[3][1]}
236
+ x2={rotatedCorners[0][0]} y2={rotatedCorners[0][1]}
237
+ on:pointerdown={grab('LEFT')} />
136
238
 
239
+ <!-- Corner handles -->
137
240
  <Handle
138
241
  class="a9s-corner-handle-topleft"
139
242
  on:pointerdown={grab('TOP_LEFT')}
140
- x={geom.x} y={geom.y}
243
+ x={rotatedCorners[0][0]} y={rotatedCorners[0][1]}
141
244
  scale={viewportScale} />
142
245
 
143
246
  <Handle
144
247
  class="a9s-corner-handle-topright"
145
248
  on:pointerdown={grab('TOP_RIGHT')}
146
- x={geom.x + geom.w} y={geom.y}
249
+ x={rotatedCorners[1][0]} y={rotatedCorners[1][1]}
147
250
  scale={viewportScale} />
148
251
 
149
252
  <Handle
150
253
  class="a9s-corner-handle-bottomright"
151
254
  on:pointerdown={grab('BOTTOM_RIGHT')}
152
- x={geom.x + geom.w} y={geom.y + geom.h}
255
+ x={rotatedCorners[2][0]} y={rotatedCorners[2][1]}
153
256
  scale={viewportScale} />
154
257
 
155
258
  <Handle
156
259
  class="a9s-corner-handle-bottomleft"
157
260
  on:pointerdown={grab('BOTTOM_LEFT')}
158
- x={geom.x} y={geom.y + geom.h}
261
+ x={rotatedCorners[3][0]} y={rotatedCorners[3][1]}
159
262
  scale={viewportScale} />
160
263
  </Editor>
161
264
 
162
265
  <style>
163
- mask.a9s-rectangle-editor-mask > rect.rect-mask-bg {
266
+ mask.a9s-rectangle-editor-mask rect.rect-mask-bg {
164
267
  fill: #fff;
165
268
  }
166
269
 
167
- mask.a9s-rectangle-editor-mask > rect.rect-mask-fg {
270
+ mask.a9s-rectangle-editor-mask polygon.rect-mask-fg {
168
271
  fill: #000;
169
272
  }
273
+
274
+ :global(.a9s-rotation-handle-line-bg) {
275
+ stroke: rgba(0, 0, 0, 0.5);
276
+ stroke-width: 1.5px;
277
+ vector-effect: non-scaling-stroke;
278
+ }
279
+
280
+ :global(.a9s-rotation-handle-line-fg) {
281
+ stroke: #fff;
282
+ stroke-width: 1px;
283
+ stroke-dasharray: 3 1;
284
+ vector-effect: non-scaling-stroke;
285
+ }
170
286
  </style>
@@ -0,0 +1,104 @@
1
+ import type { Bounds, RectangleGeometry } from '../../../model';
2
+
3
+ /**
4
+ * Rotates a point around a center by the given angle (in rad).
5
+ */
6
+ export const rotatePoint = (
7
+ point: [number, number],
8
+ center: [number, number],
9
+ angle: number
10
+ ): [number, number] => {
11
+ const [px, py] = point;
12
+ const [cx, cy] = center;
13
+
14
+ const cos = Math.cos(angle);
15
+ const sin = Math.sin(angle);
16
+
17
+ const dx = px - cx;
18
+ const dy = py - cy;
19
+
20
+ return [
21
+ cx + dx * cos - dy * sin,
22
+ cy + dx * sin + dy * cos
23
+ ];
24
+ }
25
+
26
+ /**
27
+ * Gets the four corner points of a rotated rectangle in world space.
28
+ */
29
+ export const getRotatedCorners = (
30
+ x: number,
31
+ y: number,
32
+ w: number,
33
+ h: number,
34
+ rot: number = 0
35
+ ): [[number, number], [number, number], [number, number], [number, number]] => {
36
+ const corners: [number, number][] = [
37
+ [x, y],
38
+ [x + w, y],
39
+ [x + w, y + h],
40
+ [x, y + h]
41
+ ];
42
+
43
+ const center: [number, number] = [x + w / 2, y + h / 2];
44
+
45
+ return corners.map(corner =>
46
+ rotatePoint(corner, center, rot)) as [[number, number], [number, number], [number, number], [number, number]];
47
+ }
48
+
49
+ /**
50
+ * Calculates the position of the rotation handle.
51
+ */
52
+ export const getRotationHandlePosition = (
53
+ geom: RectangleGeometry, offset: number
54
+ ): [number, number] => {
55
+ const { x , y, w, h, rot = 0 } = geom;
56
+ const center: [number, number] = [x + w / 2, y + h / 2];
57
+ let topCenter: [number, number] = [x + w / 2, y - offset];
58
+ return rotatePoint(topCenter, center, rot);
59
+ }
60
+
61
+ /**
62
+ * Transforms a movement delta from world coords to the rectangle's
63
+ * local (non-rotated) coordinate system.
64
+ */
65
+ export const transformDeltaToLocalCoords = (
66
+ deltaX: number,
67
+ deltaY: number,
68
+ rot: number
69
+ ): [number, number] => {
70
+ const cos = Math.cos(rot);
71
+ const sin = Math.sin(rot);
72
+
73
+ return [
74
+ deltaX * cos + deltaY * sin,
75
+ -deltaX * sin + deltaY * cos
76
+ ];
77
+ }
78
+
79
+ /**
80
+ * Calculates the rotation angle between a point and the origin, relative to a center.
81
+ */
82
+ export const angleFromPoints = (
83
+ point1: [number, number],
84
+ point2: [number, number],
85
+ center: [number, number]
86
+ ): number => {
87
+ const dx1 = point1[0] - center[0];
88
+ const dy1 = point1[1] - center[1];
89
+ const angle1 = Math.atan2(dy1, dx1);
90
+
91
+ const dx2 = point2[0] - center[0];
92
+ const dy2 = point2[1] - center[1];
93
+ const angle2 = Math.atan2(dy2, dx2);
94
+
95
+ return angle2 - angle1;
96
+ }
97
+
98
+ /**
99
+ * Snaps an angle to the nearest 45-degree increment
100
+ */
101
+ export const snapAngle = (angle: number, inc = 10): number => {
102
+ const step = (inc * Math.PI) / 180;
103
+ return Math.round(angle / step) * step;
104
+ }
@@ -10,23 +10,30 @@
10
10
 
11
11
  $: computedStyle = computeStyle(annotation, style);
12
12
 
13
- $: ({ x, y, w, h } = geom as RectangleGeometry);
13
+ $: ({ x, y, w, h, rot } = geom as RectangleGeometry);
14
+
15
+ // Calculate transform for rotation
16
+ $: rectTransform = (rot ?? 0) !== 0 ?
17
+ `translate(${x + w / 2}, ${y + h / 2}) rotate(${((rot ?? 0) * 180) / Math.PI}) translate(${-(x + w / 2)}, ${-(y + h / 2)})` :
18
+ undefined;
14
19
  </script>
15
20
 
16
21
  <g class="a9s-annotation" data-id={annotation.id}>
17
- <rect
18
- class="a9s-outer"
19
- style={computedStyle ? 'display:none;' : undefined}
20
- x={x}
21
- y={y}
22
- width={w}
23
- height={h} />
22
+ <g transform={rectTransform}>
23
+ <rect
24
+ class="a9s-outer"
25
+ style={computedStyle ? 'display:none;' : undefined}
26
+ x={x}
27
+ y={y}
28
+ width={w}
29
+ height={h} />
24
30
 
25
- <rect
26
- class="a9s-inner"
27
- style={computedStyle}
28
- x={x}
29
- y={y}
30
- width={w}
31
- height={h} />
31
+ <rect
32
+ class="a9s-inner"
33
+ style={computedStyle}
34
+ x={x}
35
+ y={y}
36
+ width={w}
37
+ height={h} />
38
+ </g>
32
39
  </g>
@@ -106,6 +106,7 @@
106
106
  maxX: x + w,
107
107
  maxY: y + h
108
108
  },
109
+ rot: 0,
109
110
  x, y, w, h
110
111
  }
111
112
  }
@@ -16,6 +16,8 @@ export interface RectangleGeometry extends Geometry {
16
16
 
17
17
  h: number;
18
18
 
19
+ rot?: number;
20
+
19
21
  bounds: Bounds;
20
22
 
21
23
  }
@@ -6,11 +6,37 @@ export const RectangleUtil: ShapeUtil<Rectangle> = {
6
6
 
7
7
  area: (rect: Rectangle): number => rect.geometry.w * rect.geometry.h,
8
8
 
9
- intersects: (rect: Rectangle, x: number, y: number): boolean =>
10
- x >= rect.geometry.x &&
11
- x <= rect.geometry.x + rect.geometry.w &&
12
- y >= rect.geometry.y &&
13
- y <= rect.geometry.y + rect.geometry.h
9
+ intersects: (rect: Rectangle, x: number, y: number): boolean => {
10
+ const geom = rect.geometry;
11
+
12
+ if (!geom.rot) {
13
+ return x >= geom.x &&
14
+ x <= geom.x + geom.w &&
15
+ y >= geom.y &&
16
+ y <= geom.y + geom.h;
17
+ } else {
18
+ // For rotated rectangles, transform the test point to local coordinates
19
+ const centerX = geom.x + geom.w / 2;
20
+ const centerY = geom.y + geom.h / 2;
21
+
22
+ // Translate point relative to center
23
+ const dx = x - centerX;
24
+ const dy = y - centerY;
25
+
26
+ // Rotate backwards to get to local (non-rotated) coordinates
27
+ const cos = Math.cos(geom.rot);
28
+ const sin = Math.sin(geom.rot);
29
+
30
+ const localX = dx * cos + dy * sin;
31
+ const localY = -dx * sin + dy * cos;
32
+
33
+ // Check if point is within rectangle bounds in local space
34
+ return localX >= -geom.w / 2 &&
35
+ localX <= geom.w / 2 &&
36
+ localY >= -geom.h / 2 &&
37
+ localY <= geom.h / 2;
38
+ }
39
+ }
14
40
 
15
41
  };
16
42
 
@@ -103,14 +103,16 @@ export const serializeW3CImageAnnotation = (
103
103
  let w3cSelector: FragmentSelector | SVGSelector | unknown;
104
104
 
105
105
  try {
106
- w3cSelector = selector.type == ShapeType.RECTANGLE ?
107
- serializeFragmentSelector(selector.geometry as RectangleGeometry) :
108
- serializeSVGSelector(selector);
106
+ if (selector.type === ShapeType.RECTANGLE && !(selector.geometry as RectangleGeometry).rot) {
107
+ w3cSelector = serializeFragmentSelector(selector.geometry as RectangleGeometry);
108
+ } else {
109
+ w3cSelector = serializeSVGSelector(selector);
110
+ }
109
111
  } catch (error) {
110
112
  if (opts.strict)
111
113
  throw error;
112
114
  else
113
- w3cSelector = selector;
115
+ w3cSelector = selector;
114
116
  }
115
117
 
116
118
  const serialized = {
@@ -21,7 +21,7 @@ export const isFragmentSelector = (
21
21
  const hashIndex = selector.indexOf('#');
22
22
  if (hashIndex < 0) return false;
23
23
 
24
- const xywh = /#xywh(?:=(?:pixel:|percent:)?)\s*\d+(\.\d*)?,\s*\d+(\.\d*)?,\s*\d+(\.\d*)?,\s*\d+(\.\d*)?$/i;
24
+ const xywh = /#xywh(?:=(?:pixel:|percent:)?)(.+?),(.+?),(.+?),(.+)$/i;
25
25
  return xywh.test(selector);
26
26
  }
27
27
 
@@ -55,6 +55,7 @@ export const parseFragmentSelector = (
55
55
  y,
56
56
  w,
57
57
  h,
58
+ rot: 0,
58
59
  bounds: {
59
60
  minX: x,
60
61
  minY: invertY ? y - h : y,