@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.
- package/dist/annotation/editors/rectangle/rotationUtils.d.ts +26 -0
- package/dist/annotorious.css +1 -1
- package/dist/annotorious.es.js +3454 -3296
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +1 -1
- package/dist/annotorious.js.map +1 -1
- package/dist/model/core/rectangle/Rectangle.d.ts +1 -0
- package/package.json +3 -3
- package/src/Annotorious.css +1 -0
- package/src/annotation/SVGAnnotationLayer.svelte +3 -3
- package/src/annotation/editors/rectangle/RectangleEditor.svelte +188 -68
- package/src/annotation/editors/rectangle/rotationUtils.ts +104 -0
- package/src/annotation/shapes/Rectangle.svelte +22 -15
- package/src/annotation/tools/rectangle/RubberbandRectangle.svelte +1 -0
- package/src/model/core/rectangle/Rectangle.ts +2 -0
- package/src/model/core/rectangle/rectangleUtils.ts +31 -5
- package/src/model/w3c/W3CImageFormatAdapter.ts +6 -4
- package/src/model/w3c/fragment/FragmentSelector.ts +1 -0
- package/src/model/w3c/svg/SVGSelector.ts +81 -0
- package/src/state/spatialTree.ts +2 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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": "^
|
|
62
|
+
"uuid": "^14.0.0"
|
|
63
63
|
}
|
|
64
64
|
}
|
package/src/Annotorious.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" generics="I extends Annotation, E extends unknown">
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
const
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
<
|
|
225
|
+
<line
|
|
123
226
|
class="a9s-edge-handle a9s-edge-handle-right"
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
<
|
|
128
|
-
class="a9s-edge-handle a9s-edge-handle-bottom"
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
<
|
|
133
|
-
class="a9s-edge-handle a9s-edge-handle-left"
|
|
134
|
-
|
|
135
|
-
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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
|
|
270
|
+
mask.a9s-rectangle-editor-mask rect.rect-mask-bg {
|
|
164
271
|
fill: #fff;
|
|
165
272
|
}
|
|
166
273
|
|
|
167
|
-
mask.a9s-rectangle-editor-mask
|
|
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
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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>
|
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
107
|
-
serializeFragmentSelector(selector.geometry as RectangleGeometry)
|
|
108
|
-
|
|
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
|
-
|
|
115
|
+
w3cSelector = selector;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const serialized = {
|