@annotorious/annotorious 3.7.22 → 3.8.1

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.22",
3
+ "version": "3.8.1",
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,10 +55,10 @@
55
55
  "vitest": "^3.2.4"
56
56
  },
57
57
  "dependencies": {
58
- "@annotorious/core": "3.7.22",
58
+ "@annotorious/core": "3.8.1",
59
59
  "dequal": "^2.0.3",
60
60
  "rbush": "^4.0.1",
61
61
  "simplify-js": "^1.2.4",
62
- "uuid": "^13.0.0"
62
+ "uuid": "^14.0.0"
63
63
  }
64
64
  }
@@ -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,138 @@
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
+
51
+ // Normalizes the angle to be strictly between 0 and 2π
52
+ const TWO_PI = 2 * Math.PI;
53
+ rot = ((rot % TWO_PI) + TWO_PI) % TWO_PI;
54
+ } else if (handle === 'SHAPE') {
55
+ // Moving the entire shape - translate it without rotation change
56
+ x += dx;
57
+ y += dy;
30
58
  } else {
59
+ // Edge or corner handle - resize in local (rotated) coordinate space
60
+ let localX0 = 0;
61
+ let localY0 = 0;
62
+ let localX1 = w;
63
+ let localY1 = h;
64
+
65
+ const [localDx, localDy] = rot !== 0
66
+ ? transformDeltaToLocalCoords(dx, dy, rot)
67
+ : [dx, dy];
68
+
31
69
  switch (handle) {
32
70
  case 'TOP':
33
71
  case 'TOP_LEFT':
34
- case 'TOP_RIGHT': {
35
- y0 += dy;
72
+ case 'TOP_RIGHT':
73
+ localY0 += localDy;
36
74
  break;
37
- }
38
-
39
75
  case 'BOTTOM':
40
76
  case 'BOTTOM_LEFT':
41
- case 'BOTTOM_RIGHT': {
42
- y1 += dy;
77
+ case 'BOTTOM_RIGHT':
78
+ localY1 += localDy;
43
79
  break;
44
- }
45
80
  }
46
81
 
47
82
  switch (handle) {
48
83
  case 'LEFT':
49
84
  case 'TOP_LEFT':
50
- case 'BOTTOM_LEFT': {
51
- x0 += dx;
85
+ case 'BOTTOM_LEFT':
86
+ localX0 += localDx;
52
87
  break;
53
- }
54
-
55
88
  case 'RIGHT':
56
89
  case 'TOP_RIGHT':
57
- case 'BOTTOM_RIGHT': {
58
- x1 += dx;
90
+ case 'BOTTOM_RIGHT':
91
+ localX1 += localDx;
59
92
  break;
60
- }
61
93
  }
94
+
95
+ // The center shifts as edges move - calculate new center in local space
96
+ const newLocalCx = (localX0 + localX1) / 2;
97
+ const newLocalCy = (localY0 + localY1) / 2;
98
+
99
+ w = Math.abs(localX1 - localX0);
100
+ h = Math.abs(localY1 - localY0);
101
+
102
+ // Rotate the local center offset back to world space
103
+ const oldCenter: [number, number] = [
104
+ x + (rectangle.geometry as RectangleGeometry).w / 2,
105
+ y + (rectangle.geometry as RectangleGeometry).h / 2
106
+ ];
107
+
108
+ const localCenterOffset: [number, number] = [
109
+ newLocalCx - (rectangle.geometry as RectangleGeometry).w / 2,
110
+ newLocalCy - (rectangle.geometry as RectangleGeometry).h / 2
111
+ ];
112
+
113
+ const cos = Math.cos(rot);
114
+ const sin = Math.sin(rot);
115
+
116
+ const worldCx = oldCenter[0] + localCenterOffset[0] * cos - localCenterOffset[1] * sin;
117
+ const worldCy = oldCenter[1] + localCenterOffset[0] * sin + localCenterOffset[1] * cos;
118
+
119
+ x = worldCx - w / 2;
120
+ y = worldCy - h / 2;
62
121
  }
63
122
 
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);
123
+ // Calculate new bounds
124
+ const bounds = boundsFromPoints(rotatedCorners);
68
125
 
69
126
  return {
70
127
  ...rectangle,
71
128
  geometry: {
72
- x, y, w, h,
73
- bounds: {
74
- minX: x,
75
- minY: y,
76
- maxX: x + w,
77
- maxY: y + h
78
- }
129
+ x, y, w, h, rot,
130
+ bounds
79
131
  }
80
132
  };
81
133
  }
82
134
 
83
- $: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
135
+ onMount(() => {
136
+ // Track SHIFT key
137
+ const onKeyDown = (evt: KeyboardEvent) => {
138
+ if (evt.key === 'Shift') shiftPressed = true;
139
+ }
140
+
141
+ const onKeyUp = (evt: KeyboardEvent) => {
142
+ if (evt.key === 'Shift') shiftPressed = false;
143
+ }
144
+
145
+ window.addEventListener('keydown', onKeyDown);
146
+ window.addEventListener('keyup', onKeyUp);
147
+
148
+ return () => {
149
+ window.removeEventListener('keydown', onKeyDown);
150
+ window.removeEventListener('keyup', onKeyUp);
151
+ };
152
+ });
153
+
154
+ $: mask = getMaskDimensions(geom.bounds, 5 / viewportScale);
84
155
 
85
156
  const maskId = `rect-mask-${Math.random().toString(36).substring(2, 12)}`;
86
157
  </script>
@@ -98,73 +169,122 @@
98
169
  <defs>
99
170
  <mask id={maskId} class="a9s-rectangle-editor-mask">
100
171
  <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} />
172
+ <polygon
173
+ class="rect-mask-fg"
174
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
102
175
  </mask>
103
176
  </defs>
104
177
 
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} />
178
+ <!-- Rotation handle -->
179
+ <g class="a9s-rotation-handle-group">
180
+ <line
181
+ class="a9s-rotation-handle-line-bg"
182
+ x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
183
+ y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
184
+ x2={rotationHandlePos[0]}
185
+ y2={rotationHandlePos[1]}
186
+ pointer-events="none" />
187
+
188
+ <line
189
+ class="a9s-rotation-handle-line-fg"
190
+ x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
191
+ y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
192
+ x2={rotationHandlePos[0]}
193
+ y2={rotationHandlePos[1]}
194
+ pointer-events="none" />
195
+
196
+ <Handle
197
+ class="a9s-rotation-handle"
198
+ on:pointerdown={grab('ROTATION')}
199
+ x={rotationHandlePos[0]} y={rotationHandlePos[1]}
200
+ scale={viewportScale} />
201
+ </g>
202
+
203
+ <!-- Rectangle shape -->
204
+ <g>
205
+ <polygon
206
+ class="a9s-outer"
207
+ mask={`url(#${maskId})`}
208
+ on:pointerdown={grab('SHAPE')}
209
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
110
210
 
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} />
211
+ <polygon
212
+ class="a9s-inner a9s-shape-handle"
213
+ style={computedStyle}
214
+ on:pointerdown={grab('SHAPE')}
215
+ points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
216
+ </g>
116
217
 
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} />
218
+ <!-- Edge handles -->
219
+ <line
220
+ class="a9s-edge-handle a9s-edge-handle-top"
221
+ x1={rotatedCorners[0][0]} y1={rotatedCorners[0][1]}
222
+ x2={rotatedCorners[1][0]} y2={rotatedCorners[1][1]}
223
+ on:pointerdown={grab('TOP')} />
121
224
 
122
- <rect
225
+ <line
123
226
  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}/>
227
+ x1={rotatedCorners[1][0]} y1={rotatedCorners[1][1]}
228
+ x2={rotatedCorners[2][0]} y2={rotatedCorners[2][1]}
229
+ on:pointerdown={grab('RIGHT')} />
126
230
 
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} />
231
+ <line
232
+ class="a9s-edge-handle a9s-edge-handle-bottom"
233
+ x1={rotatedCorners[2][0]} y1={rotatedCorners[2][1]}
234
+ x2={rotatedCorners[3][0]} y2={rotatedCorners[3][1]}
235
+ on:pointerdown={grab('BOTTOM')} />
131
236
 
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} />
237
+ <line
238
+ class="a9s-edge-handle a9s-edge-handle-left"
239
+ x1={rotatedCorners[3][0]} y1={rotatedCorners[3][1]}
240
+ x2={rotatedCorners[0][0]} y2={rotatedCorners[0][1]}
241
+ on:pointerdown={grab('LEFT')} />
136
242
 
243
+ <!-- Corner handles -->
137
244
  <Handle
138
245
  class="a9s-corner-handle-topleft"
139
246
  on:pointerdown={grab('TOP_LEFT')}
140
- x={geom.x} y={geom.y}
247
+ x={rotatedCorners[0][0]} y={rotatedCorners[0][1]}
141
248
  scale={viewportScale} />
142
249
 
143
250
  <Handle
144
251
  class="a9s-corner-handle-topright"
145
252
  on:pointerdown={grab('TOP_RIGHT')}
146
- x={geom.x + geom.w} y={geom.y}
253
+ x={rotatedCorners[1][0]} y={rotatedCorners[1][1]}
147
254
  scale={viewportScale} />
148
255
 
149
256
  <Handle
150
257
  class="a9s-corner-handle-bottomright"
151
258
  on:pointerdown={grab('BOTTOM_RIGHT')}
152
- x={geom.x + geom.w} y={geom.y + geom.h}
259
+ x={rotatedCorners[2][0]} y={rotatedCorners[2][1]}
153
260
  scale={viewportScale} />
154
261
 
155
262
  <Handle
156
263
  class="a9s-corner-handle-bottomleft"
157
264
  on:pointerdown={grab('BOTTOM_LEFT')}
158
- x={geom.x} y={geom.y + geom.h}
265
+ x={rotatedCorners[3][0]} y={rotatedCorners[3][1]}
159
266
  scale={viewportScale} />
160
267
  </Editor>
161
268
 
162
269
  <style>
163
- mask.a9s-rectangle-editor-mask > rect.rect-mask-bg {
270
+ mask.a9s-rectangle-editor-mask rect.rect-mask-bg {
164
271
  fill: #fff;
165
272
  }
166
273
 
167
- mask.a9s-rectangle-editor-mask > rect.rect-mask-fg {
274
+ mask.a9s-rectangle-editor-mask polygon.rect-mask-fg {
168
275
  fill: #000;
169
276
  }
277
+
278
+ :global(.a9s-rotation-handle-line-bg) {
279
+ stroke: rgba(0, 0, 0, 0.5);
280
+ stroke-width: 1.5px;
281
+ vector-effect: non-scaling-stroke;
282
+ }
283
+
284
+ :global(.a9s-rotation-handle-line-fg) {
285
+ stroke: #fff;
286
+ stroke-width: 1px;
287
+ stroke-dasharray: 3 1;
288
+ vector-effect: non-scaling-stroke;
289
+ }
170
290
  </style>
@@ -0,0 +1,104 @@
1
+ import type { 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 = {
@@ -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,