@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.
- package/dist/annotation/editors/rectangle/rotationUtils.d.ts +26 -0
- package/dist/annotorious.css +1 -1
- package/dist/annotorious.es.js +3362 -3209
- 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 +2 -2
- package/src/Annotorious.css +1 -0
- package/src/annotation/SVGAnnotationLayer.svelte +3 -3
- package/src/annotation/editors/rectangle/RectangleEditor.svelte +184 -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 +2 -1
- 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.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.
|
|
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",
|
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,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
|
-
|
|
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
|
+
} 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
const
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
<
|
|
128
|
-
class="a9s-edge-handle a9s-edge-handle-bottom"
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
<
|
|
133
|
-
class="a9s-edge-handle a9s-edge-handle-left"
|
|
134
|
-
|
|
135
|
-
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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
|
|
266
|
+
mask.a9s-rectangle-editor-mask rect.rect-mask-bg {
|
|
164
267
|
fill: #fff;
|
|
165
268
|
}
|
|
166
269
|
|
|
167
|
-
mask.a9s-rectangle-editor-mask
|
|
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
|
-
<
|
|
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 = {
|
|
@@ -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:)?)
|
|
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,
|