@annotorious/annotorious 3.3.5 → 3.4.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/polygon/MidpointHandle.svelte.d.ts +1 -0
- package/dist/annotation/utils/index.d.ts +2 -0
- package/dist/annotation/utils/svg.d.ts +7 -0
- package/dist/annotorious.css +1 -1
- package/dist/annotorious.es.js +3917 -3121
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +2 -2
- package/dist/annotorious.js.map +1 -1
- package/dist/state/ImageAnnotationStore.d.ts +3 -4
- package/dist/state/spatialTree.d.ts +2 -2
- package/package.json +6 -6
- package/src/Annotorious.css +4 -0
- package/src/annotation/SVGAnnotationLayer.svelte +4 -1
- package/src/annotation/editors/Editor.svelte +17 -1
- package/src/annotation/editors/EditorMount.svelte +7 -1
- package/src/annotation/editors/Handle.svelte +61 -33
- package/src/annotation/editors/multipolygon/MultiPolygonEditor.svelte +27 -3
- package/src/annotation/editors/polygon/MidpointHandle.svelte +76 -0
- package/src/annotation/editors/polygon/PolygonEditor.svelte +274 -9
- package/src/annotation/editors/rectangle/RectangleEditor.svelte +26 -2
- package/src/annotation/tools/polygon/RubberbandPolygon.svelte +49 -17
- package/src/annotation/tools/rectangle/RubberbandRectangle.svelte +36 -2
- package/src/annotation/utils/index.ts +2 -0
- package/src/annotation/utils/svg.ts +11 -0
- package/src/state/ImageAnnotationStore.ts +3 -4
- package/src/state/ImageAnnotatorState.ts +16 -4
- package/src/state/spatialTree.ts +6 -3
- package/src/themes/light/index.css +2 -2
|
@@ -1,64 +1,329 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher, onMount, tick } from 'svelte';
|
|
2
3
|
import { boundsFromPoints } from '../../../model';
|
|
3
4
|
import type { Polygon, PolygonGeometry, Shape } from '../../../model';
|
|
5
|
+
import { getMaskDimensions } from '../../utils';
|
|
4
6
|
import type { Transform } from '../../Transform';
|
|
5
|
-
import
|
|
7
|
+
import Editor from '../Editor.svelte';
|
|
8
|
+
import Handle from '../Handle.svelte';
|
|
9
|
+
import Midpoint from './MidpointHandle.svelte';
|
|
10
|
+
|
|
11
|
+
const dispatch = createEventDispatcher<{ change: Polygon }>();
|
|
12
|
+
|
|
13
|
+
/** Time difference (milliseconds) required for registering a click/tap **/
|
|
14
|
+
const CLICK_THRESHOLD = 250;
|
|
15
|
+
|
|
16
|
+
/** Minimum distance (px) to shape required for midpoints to show */
|
|
17
|
+
const MIN_HOVER_DISTANCE = 1000;
|
|
18
|
+
|
|
19
|
+
/** Minimum distance (px) between corners required for midpoints to show **/
|
|
20
|
+
const MIN_CORNER_DISTANCE = 12;
|
|
21
|
+
|
|
22
|
+
/** Needed for the <mask> element **/
|
|
23
|
+
const MIDPOINT_SIZE = 4.5;
|
|
6
24
|
|
|
7
25
|
/** Props */
|
|
8
26
|
export let shape: Polygon;
|
|
9
27
|
export let computedStyle: string | undefined;
|
|
10
28
|
export let transform: Transform;
|
|
11
29
|
export let viewportScale: number = 1;
|
|
30
|
+
export let svgEl: SVGSVGElement;
|
|
31
|
+
|
|
32
|
+
/** Drawing tool layer **/
|
|
33
|
+
let polygonEl: SVGGElement;
|
|
34
|
+
let visibleMidpoint: number | undefined;
|
|
35
|
+
let isHandleHovered = false;
|
|
36
|
+
let lastHandleClick: number | undefined;
|
|
37
|
+
let selectedCorners: number[] = [];
|
|
12
38
|
|
|
13
39
|
$: geom = shape.geometry;
|
|
14
40
|
|
|
41
|
+
$: midpoints = geom.points.map((thisCorner, idx) => {
|
|
42
|
+
const nextCorner = idx === geom.points.length - 1 ? geom.points[0] : geom.points[idx + 1];
|
|
43
|
+
|
|
44
|
+
const x = (thisCorner[0] + nextCorner[0]) / 2;
|
|
45
|
+
const y = (thisCorner[1] + nextCorner[1]) / 2;
|
|
46
|
+
|
|
47
|
+
const dist = Math.sqrt(
|
|
48
|
+
Math.pow(nextCorner[0] - x, 2) + Math.pow(nextCorner[1] - y, 2));
|
|
49
|
+
|
|
50
|
+
// Don't show if the distance between the corners is too small
|
|
51
|
+
const visible = dist > MIN_CORNER_DISTANCE / viewportScale;
|
|
52
|
+
|
|
53
|
+
return { point: [x, y], visible };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Handle hover state **/
|
|
57
|
+
const onEnterHandle = () => isHandleHovered = true;
|
|
58
|
+
const onLeaveHandle = () => isHandleHovered = false;
|
|
59
|
+
|
|
60
|
+
/** Determine visible midpoint, if any **/
|
|
61
|
+
const onPointerMove = (evt: PointerEvent) => {
|
|
62
|
+
if (selectedCorners.length > 0) {
|
|
63
|
+
visibleMidpoint = undefined;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [px, py] = transform.elementToImage(evt.offsetX, evt.offsetY);
|
|
68
|
+
|
|
69
|
+
const getDistSq = (pt: number[]) =>
|
|
70
|
+
Math.pow(pt[0] - px, 2) + Math.pow(pt[1] - py, 2);
|
|
71
|
+
|
|
72
|
+
const closestCorner = geom.points.reduce((closest, corner) =>
|
|
73
|
+
getDistSq(corner) < getDistSq(closest) ? corner : closest);
|
|
74
|
+
|
|
75
|
+
const closestVisibleMidpoint = midpoints
|
|
76
|
+
.filter(m => m.visible)
|
|
77
|
+
.reduce((closest, midpoint) =>
|
|
78
|
+
getDistSq(midpoint.point) < getDistSq(closest.point) ? midpoint : closest);
|
|
79
|
+
|
|
80
|
+
// Show midpoint of the mouse is at least within THRESHOLD distance
|
|
81
|
+
// of the midpoint or the closest corner. (Basically a poor man's shape buffering).
|
|
82
|
+
const threshold = Math.pow(MIN_HOVER_DISTANCE / viewportScale, 2);
|
|
83
|
+
|
|
84
|
+
const shouldShow =
|
|
85
|
+
getDistSq(closestCorner) < threshold ||
|
|
86
|
+
getDistSq(closestVisibleMidpoint.point) < threshold;
|
|
87
|
+
|
|
88
|
+
if (shouldShow)
|
|
89
|
+
visibleMidpoint = midpoints.indexOf(closestVisibleMidpoint);
|
|
90
|
+
else
|
|
91
|
+
visibleMidpoint = undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SVG element keeps loosing focus when interacting with
|
|
96
|
+
* shapes–this function refocuses.
|
|
97
|
+
*/
|
|
98
|
+
const reclaimFocus = () => {
|
|
99
|
+
if (document.activeElement !== svgEl)
|
|
100
|
+
svgEl.focus();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* De-selects all corners and reclaims focus.
|
|
105
|
+
*/
|
|
106
|
+
const onShapePointerUp = () => {
|
|
107
|
+
selectedCorners = [];
|
|
108
|
+
reclaimFocus();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Updates state, waiting for potential click.
|
|
113
|
+
*/
|
|
114
|
+
const onHandlePointerDown = (evt: PointerEvent) => {
|
|
115
|
+
isHandleHovered = true;
|
|
116
|
+
|
|
117
|
+
evt.preventDefault();
|
|
118
|
+
evt.stopPropagation();
|
|
119
|
+
|
|
120
|
+
lastHandleClick = performance.now();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Selection handling logic **/
|
|
124
|
+
const onHandlePointerUp = (idx: number) => (evt: PointerEvent) => {
|
|
125
|
+
if (!lastHandleClick) return;
|
|
126
|
+
|
|
127
|
+
// Drag, not click
|
|
128
|
+
if (performance.now() - lastHandleClick > CLICK_THRESHOLD) return;
|
|
129
|
+
|
|
130
|
+
const isSelected = selectedCorners.includes(idx);
|
|
131
|
+
|
|
132
|
+
if (evt.metaKey || evt.ctrlKey || evt.shiftKey) {
|
|
133
|
+
// Add to or remove from selection
|
|
134
|
+
if (isSelected)
|
|
135
|
+
selectedCorners = selectedCorners.filter(i => i !== idx);
|
|
136
|
+
else
|
|
137
|
+
selectedCorners = [...selectedCorners, idx];
|
|
138
|
+
} else {
|
|
139
|
+
if (isSelected && selectedCorners.length > 1)
|
|
140
|
+
// Keep selected, de-select others
|
|
141
|
+
selectedCorners = [idx]
|
|
142
|
+
else if (isSelected)
|
|
143
|
+
// De-select
|
|
144
|
+
selectedCorners = [];
|
|
145
|
+
else
|
|
146
|
+
selectedCorners = [idx];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
reclaimFocus();
|
|
150
|
+
}
|
|
151
|
+
|
|
15
152
|
const editor = (polygon: Shape, handle: string, delta: [number, number]) => {
|
|
153
|
+
reclaimFocus();
|
|
154
|
+
|
|
16
155
|
let points: [number, number][];
|
|
17
156
|
|
|
18
157
|
const geom = (polygon.geometry) as PolygonGeometry;
|
|
19
158
|
|
|
20
|
-
if (
|
|
159
|
+
if (selectedCorners.length > 1) {
|
|
160
|
+
points = geom.points.map(([x, y], idx) =>
|
|
161
|
+
selectedCorners.includes(idx) ? [x + delta[0], y + delta[1]] : [x, y]);
|
|
162
|
+
} else if (handle === 'SHAPE') {
|
|
21
163
|
points = geom.points.map(([x, y]) => [x + delta[0], y + delta[1]]);
|
|
22
164
|
} else {
|
|
23
165
|
points = geom.points.map(([x, y], idx) =>
|
|
24
|
-
handle === `HANDLE-${idx}` ? [x + delta[0], y + delta[1]] : [x, y]
|
|
25
|
-
);
|
|
166
|
+
handle === `HANDLE-${idx}` ? [x + delta[0], y + delta[1]] : [x, y]);
|
|
26
167
|
}
|
|
27
168
|
|
|
28
169
|
const bounds = boundsFromPoints(points);
|
|
29
|
-
|
|
30
170
|
return {
|
|
31
171
|
...polygon,
|
|
32
172
|
geometry: { points, bounds }
|
|
33
173
|
}
|
|
34
174
|
}
|
|
175
|
+
|
|
176
|
+
const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => {
|
|
177
|
+
evt.stopPropagation();
|
|
178
|
+
|
|
179
|
+
const points = [
|
|
180
|
+
...geom.points.slice(0, midpointIdx + 1),
|
|
181
|
+
midpoints[midpointIdx].point,
|
|
182
|
+
...geom.points.slice(midpointIdx + 1)
|
|
183
|
+
] as [number, number][];
|
|
184
|
+
|
|
185
|
+
const bounds = boundsFromPoints(points);
|
|
186
|
+
|
|
187
|
+
dispatch('change', {
|
|
188
|
+
...shape,
|
|
189
|
+
geometry: { points, bounds }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await tick();
|
|
193
|
+
|
|
194
|
+
// Find the newly inserted handle and dispatch grab event
|
|
195
|
+
const newHandle = [...document.querySelectorAll(`.a9s-handle`)][midpointIdx + 1];
|
|
196
|
+
if (newHandle?.firstChild) {
|
|
197
|
+
const newEvent = new PointerEvent('pointerdown', {
|
|
198
|
+
bubbles: true,
|
|
199
|
+
cancelable: true,
|
|
200
|
+
clientX: evt.clientX,
|
|
201
|
+
clientY: evt.clientY,
|
|
202
|
+
pointerId: evt.pointerId,
|
|
203
|
+
pointerType: evt.pointerType,
|
|
204
|
+
isPrimary: evt.isPrimary,
|
|
205
|
+
buttons: evt.buttons
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
newHandle.firstChild.dispatchEvent(newEvent);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const onDeleteSelected = () => {
|
|
213
|
+
// Polygon needs 3 points min
|
|
214
|
+
if (geom.points.length < 4) return;
|
|
215
|
+
|
|
216
|
+
const points = geom.points.filter((_, i) => !selectedCorners.includes(i)) as [number, number][];
|
|
217
|
+
const bounds = boundsFromPoints(points);
|
|
218
|
+
|
|
219
|
+
dispatch('change', {
|
|
220
|
+
...shape,
|
|
221
|
+
geometry: { points, bounds }
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
selectedCorners = [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
onMount(() => {
|
|
228
|
+
const onKeydown = (evt: KeyboardEvent) => {
|
|
229
|
+
if (evt.key === 'Delete' || evt.key === 'Backspace') {
|
|
230
|
+
evt.preventDefault();
|
|
231
|
+
onDeleteSelected();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
svgEl.addEventListener('pointermove', onPointerMove);
|
|
236
|
+
svgEl.addEventListener('keydown', onKeydown);
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
svgEl.removeEventListener('pointermove', onPointerMove);
|
|
240
|
+
svgEl.removeEventListener('keydown', onKeydown);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
$: mask = getMaskDimensions(geom.bounds, MIDPOINT_SIZE / viewportScale);
|
|
245
|
+
|
|
246
|
+
const maskId = `polygon-mask-${Math.random().toString(36).substring(2, 12)}`;
|
|
35
247
|
</script>
|
|
36
248
|
|
|
37
249
|
<Editor
|
|
38
250
|
shape={shape}
|
|
39
251
|
transform={transform}
|
|
40
252
|
editor={editor}
|
|
253
|
+
svgEl={svgEl}
|
|
41
254
|
on:change
|
|
42
255
|
on:grab
|
|
43
256
|
on:release
|
|
44
257
|
let:grab={grab}>
|
|
258
|
+
|
|
259
|
+
<defs>
|
|
260
|
+
<mask id={`${maskId}-outer`} class="a9s-polygon-editor-mask">
|
|
261
|
+
<rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
|
|
262
|
+
<polygon points={geom.points.map(xy => xy.join(',')).join(' ')} />
|
|
263
|
+
|
|
264
|
+
{#if (visibleMidpoint !== undefined && !isHandleHovered)}
|
|
265
|
+
{@const { point } = midpoints[visibleMidpoint]}
|
|
266
|
+
<circle cx={point[0]} cy={point[1]} r={MIDPOINT_SIZE / viewportScale} />
|
|
267
|
+
{/if}
|
|
268
|
+
</mask>
|
|
269
|
+
|
|
270
|
+
{#if (visibleMidpoint !== undefined && !isHandleHovered)}
|
|
271
|
+
{@const { point } = midpoints[visibleMidpoint]}
|
|
272
|
+
<mask id={`${maskId}-inner`} class="a9s-polygon-editor-mask">
|
|
273
|
+
<rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
|
|
274
|
+
<circle cx={point[0]} cy={point[1]} r={MIDPOINT_SIZE / viewportScale} />
|
|
275
|
+
</mask>
|
|
276
|
+
{/if}
|
|
277
|
+
</defs>
|
|
45
278
|
|
|
46
279
|
<polygon
|
|
47
280
|
class="a9s-outer"
|
|
48
|
-
|
|
281
|
+
mask={`url(#${maskId}-outer)`}
|
|
282
|
+
on:pointerup={onShapePointerUp}
|
|
49
283
|
on:pointerdown={grab('SHAPE')}
|
|
50
284
|
points={geom.points.map(xy => xy.join(',')).join(' ')} />
|
|
51
285
|
|
|
52
286
|
<polygon
|
|
287
|
+
bind:this={polygonEl}
|
|
53
288
|
class="a9s-inner a9s-shape-handle"
|
|
289
|
+
mask={`url(#${maskId}-inner)`}
|
|
54
290
|
style={computedStyle}
|
|
291
|
+
on:pointermove={onPointerMove}
|
|
292
|
+
on:pointerup={onShapePointerUp}
|
|
55
293
|
on:pointerdown={grab('SHAPE')}
|
|
56
294
|
points={geom.points.map(xy => xy.join(',')).join(' ')} />
|
|
57
295
|
|
|
58
296
|
{#each geom.points as point, idx}
|
|
59
297
|
<Handle
|
|
298
|
+
class="a9s-corner-handle"
|
|
299
|
+
x={point[0]}
|
|
300
|
+
y={point[1]}
|
|
301
|
+
scale={viewportScale}
|
|
302
|
+
selected={selectedCorners.includes(idx)}
|
|
303
|
+
on:pointerenter={onEnterHandle}
|
|
304
|
+
on:pointerleave={onLeaveHandle}
|
|
305
|
+
on:pointerdown={onHandlePointerDown}
|
|
60
306
|
on:pointerdown={grab(`HANDLE-${idx}`)}
|
|
61
|
-
|
|
62
|
-
scale={viewportScale} />
|
|
307
|
+
on:pointerup={onHandlePointerUp(idx)} />
|
|
63
308
|
{/each}
|
|
64
|
-
|
|
309
|
+
|
|
310
|
+
{#if (visibleMidpoint !== undefined && !isHandleHovered)}
|
|
311
|
+
{@const { point } = midpoints[visibleMidpoint]}
|
|
312
|
+
<Midpoint
|
|
313
|
+
x={point[0]}
|
|
314
|
+
y={point[1]}
|
|
315
|
+
scale={viewportScale}
|
|
316
|
+
on:pointerdown={onAddPoint(visibleMidpoint)} />
|
|
317
|
+
{/if}
|
|
318
|
+
</Editor>
|
|
319
|
+
|
|
320
|
+
<style>
|
|
321
|
+
mask.a9s-polygon-editor-mask > rect {
|
|
322
|
+
fill: #fff;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
mask.a9s-polygon-editor-mask > circle,
|
|
326
|
+
mask.a9s-polygon-editor-mask > polygon {
|
|
327
|
+
fill: #000;
|
|
328
|
+
}
|
|
329
|
+
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Handle from '../Handle.svelte';
|
|
3
|
+
import { getMaskDimensions } from '../../utils';
|
|
3
4
|
import type { Rectangle, Shape } from '../../../model';
|
|
4
5
|
import type { Transform } from '../../Transform';
|
|
5
6
|
import { Editor } from '..';
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
export let computedStyle: string | undefined;
|
|
10
11
|
export let transform: Transform;
|
|
11
12
|
export let viewportScale: number = 1;
|
|
13
|
+
export let svgEl: SVGSVGElement;
|
|
12
14
|
|
|
13
15
|
$: geom = shape.geometry;
|
|
14
16
|
|
|
@@ -77,20 +79,32 @@
|
|
|
77
79
|
}
|
|
78
80
|
};
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
$: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
|
|
84
|
+
|
|
85
|
+
const maskId = `rect-mask-${Math.random().toString(36).substring(2, 12)}`;
|
|
80
86
|
</script>
|
|
81
87
|
|
|
82
88
|
<Editor
|
|
83
89
|
shape={shape}
|
|
84
90
|
transform={transform}
|
|
85
91
|
editor={editor}
|
|
92
|
+
svgEl={svgEl}
|
|
86
93
|
on:grab
|
|
87
94
|
on:change
|
|
88
95
|
on:release
|
|
89
96
|
let:grab={grab}>
|
|
90
97
|
|
|
98
|
+
<defs>
|
|
99
|
+
<mask id={maskId} class="a9s-rectangle-editor-mask">
|
|
100
|
+
<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} />
|
|
102
|
+
</mask>
|
|
103
|
+
</defs>
|
|
104
|
+
|
|
91
105
|
<rect
|
|
92
106
|
class="a9s-outer"
|
|
93
|
-
|
|
107
|
+
mask={`url(#${maskId})`}
|
|
94
108
|
on:pointerdown={grab('SHAPE')}
|
|
95
109
|
x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
|
|
96
110
|
|
|
@@ -143,4 +157,14 @@
|
|
|
143
157
|
on:pointerdown={grab('BOTTOM_LEFT')}
|
|
144
158
|
x={geom.x} y={geom.y + geom.h}
|
|
145
159
|
scale={viewportScale} />
|
|
146
|
-
</Editor>
|
|
160
|
+
</Editor>
|
|
161
|
+
|
|
162
|
+
<style>
|
|
163
|
+
mask.a9s-rectangle-editor-mask > rect.rect-mask-bg {
|
|
164
|
+
fill: #fff;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
mask.a9s-rectangle-editor-mask > rect.rect-mask-fg {
|
|
168
|
+
fill: #000;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { onMount, createEventDispatcher } from 'svelte';
|
|
3
3
|
import type { DrawingMode } from '../../../AnnotoriousOpts';
|
|
4
4
|
import { boundsFromPoints, computeArea, ShapeType, type Polygon } from '../../../model';
|
|
5
|
-
import { distance } from '../../utils';
|
|
5
|
+
import { distance, getMaskDimensions } from '../../utils';
|
|
6
6
|
import type { Transform } from '../..';
|
|
7
7
|
|
|
8
8
|
const dispatch = createEventDispatcher<{ create: Polygon }>();
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
const TOUCH_PAUSE_LIMIT = 1500;
|
|
32
32
|
|
|
33
|
-
$:
|
|
33
|
+
$: handleRadius = 4 / viewportScale;
|
|
34
34
|
|
|
35
35
|
const onPointerDown = (event: Event) => {
|
|
36
36
|
const evt = event as PointerEvent;
|
|
@@ -167,26 +167,58 @@
|
|
|
167
167
|
addEventListener('pointerup', onPointerUp, true);
|
|
168
168
|
addEventListener('dblclick', onDblClick, true);
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
$: coords = cursor ? (isClosable ? points : [...points, cursor]) : [];
|
|
172
|
+
|
|
173
|
+
$: mask = coords.length > 0 ? getMaskDimensions(boundsFromPoints(coords), 2 / viewportScale) : undefined;
|
|
174
|
+
|
|
175
|
+
const maskId = `polygon-mask-${Math.random().toString(36).substring(2, 12)}`;
|
|
170
176
|
</script>
|
|
171
177
|
|
|
172
178
|
<g class="a9s-annotation a9s-rubberband">
|
|
173
|
-
{#if
|
|
174
|
-
{@const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
{#if mask}
|
|
180
|
+
{@const str = coords.map(xy => xy.join(',')).join(' ')}
|
|
181
|
+
|
|
182
|
+
<defs>
|
|
183
|
+
<mask id={maskId} class="a9s-rubberband-polygon-mask">
|
|
184
|
+
<rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
|
|
185
|
+
<polygon points={str} />
|
|
186
|
+
</mask>
|
|
187
|
+
</defs>
|
|
188
|
+
|
|
189
|
+
<polygon
|
|
190
|
+
class="a9s-outer"
|
|
191
|
+
mask={`url(#${maskId})`}
|
|
192
|
+
points={str} />
|
|
193
|
+
|
|
194
|
+
<polygon
|
|
195
|
+
class="a9s-inner"
|
|
196
|
+
points={str} />
|
|
182
197
|
|
|
183
198
|
{#if isClosable}
|
|
184
|
-
<
|
|
199
|
+
<circle
|
|
185
200
|
class="a9s-handle"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
width={handleSize} />
|
|
201
|
+
cx={points[0][0]}
|
|
202
|
+
cy={points[0][1]}
|
|
203
|
+
r={handleRadius} />
|
|
190
204
|
{/if}
|
|
191
205
|
{/if}
|
|
192
|
-
</g>
|
|
206
|
+
</g>
|
|
207
|
+
|
|
208
|
+
<style>
|
|
209
|
+
mask.a9s-rubberband-polygon-mask > rect {
|
|
210
|
+
fill: #fff;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mask.a9s-rubberband-polygon-mask > polygon {
|
|
214
|
+
fill: #000;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
circle.a9s-handle {
|
|
218
|
+
fill: #fff;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
stroke: rgba(0, 0, 0, 0.35);
|
|
221
|
+
stroke-width: 1px;
|
|
222
|
+
vector-effect: non-scaling-stroke;
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createEventDispatcher, onMount } from 'svelte';
|
|
3
3
|
import type { DrawingMode } from '../../../AnnotoriousOpts';
|
|
4
4
|
import { ShapeType, type Rectangle } from '../../../model';
|
|
5
|
-
import type
|
|
5
|
+
import { getMaskDimensions, type Transform } from '../..';
|
|
6
6
|
|
|
7
7
|
const dispatch = createEventDispatcher<{ create: Rectangle }>();
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export let addEventListener: (type: string, fn: EventListener, capture?: boolean) => void;
|
|
11
11
|
export let drawingMode: DrawingMode;
|
|
12
12
|
export let transform: Transform;
|
|
13
|
+
export let viewportScale: number = 1;
|
|
13
14
|
|
|
14
15
|
let lastPointerDown: number;
|
|
15
16
|
|
|
@@ -121,12 +122,35 @@
|
|
|
121
122
|
addEventListener('pointermove', onPointerMove);
|
|
122
123
|
addEventListener('pointerup', onPointerUp, true);
|
|
123
124
|
});
|
|
125
|
+
|
|
126
|
+
const maskId = `rect-mask-${Math.random().toString(36).substring(2, 12)}`;
|
|
127
|
+
|
|
128
|
+
$: buffer = 2 / viewportScale;
|
|
124
129
|
</script>
|
|
125
130
|
|
|
126
131
|
<g class="a9s-annotation a9s-rubberband">
|
|
127
132
|
{#if origin}
|
|
133
|
+
<defs>
|
|
134
|
+
<mask id={maskId} class="a9s-rubberband-rectangle-mask">
|
|
135
|
+
<rect
|
|
136
|
+
class="rect-mask-bg"
|
|
137
|
+
x={x - buffer}
|
|
138
|
+
y={y - buffer}
|
|
139
|
+
width={w + 2 * buffer}
|
|
140
|
+
height={h + 2 * buffer}/>
|
|
141
|
+
|
|
142
|
+
<rect
|
|
143
|
+
class="rect-mask-fg"
|
|
144
|
+
x={x}
|
|
145
|
+
y={y}
|
|
146
|
+
width={w}
|
|
147
|
+
height={h} />
|
|
148
|
+
</mask>
|
|
149
|
+
</defs>
|
|
150
|
+
|
|
128
151
|
<rect
|
|
129
152
|
class="a9s-outer"
|
|
153
|
+
mask={`url(#${maskId})`}
|
|
130
154
|
x={x}
|
|
131
155
|
y={y}
|
|
132
156
|
width={w}
|
|
@@ -139,4 +163,14 @@
|
|
|
139
163
|
width={w}
|
|
140
164
|
height={h} />
|
|
141
165
|
{/if}
|
|
142
|
-
</g>
|
|
166
|
+
</g>
|
|
167
|
+
|
|
168
|
+
<style>
|
|
169
|
+
mask.a9s-rubberband-rectangle-mask > rect.rect-mask-bg {
|
|
170
|
+
fill: #fff;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
mask.a9s-rubberband-rectangle-mask > rect.rect-mask-fg {
|
|
174
|
+
fill: #000;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Bounds } from '../../model';
|
|
2
|
+
|
|
3
|
+
export const getMaskDimensions = (bounds: Bounds, buffer: number = 0) => {
|
|
4
|
+
const { minX, minY, maxX, maxY } = bounds;
|
|
5
|
+
return {
|
|
6
|
+
x: minX - buffer,
|
|
7
|
+
y: minY - buffer,
|
|
8
|
+
w: maxX - minX + 2 * buffer,
|
|
9
|
+
h: maxY - minY + 2 * buffer
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { Annotation, Store, SvelteAnnotatorState, SvelteStore } from '@annotorious/core';
|
|
2
|
-
import type { ImageAnnotation } from '../model';
|
|
1
|
+
import type { Annotation, Filter, Store, SvelteAnnotatorState, SvelteStore } from '@annotorious/core';
|
|
3
2
|
|
|
4
3
|
export type ImageAnnotationStore<I extends Annotation> = Store<I> & {
|
|
5
4
|
|
|
6
|
-
getAt(x: number, y: number):
|
|
5
|
+
getAt(x: number, y: number, filter?: Filter<I>): I | undefined;
|
|
7
6
|
|
|
8
|
-
getIntersecting(x: number, y: number, width: number, height: number):
|
|
7
|
+
getIntersecting(x: number, y: number, width: number, height: number): I[];
|
|
9
8
|
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
toSvelteStore,
|
|
7
7
|
type Annotation,
|
|
8
8
|
type AnnotatorState,
|
|
9
|
+
type Filter,
|
|
9
10
|
type HoverState,
|
|
10
11
|
type SelectionState
|
|
11
12
|
} from '@annotorious/core';
|
|
@@ -53,13 +54,24 @@ export const createImageAnnotatorState = <I extends Annotation, E extends unknow
|
|
|
53
54
|
tree.update(oldValue.target, newValue.target));
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
const getAt = (x: number, y: number):
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const getAt = (x: number, y: number, filter?: Filter<I>): I | undefined => {
|
|
58
|
+
const targets = tree.getAt(x, y, filter as Filter<Annotation>);
|
|
59
|
+
|
|
60
|
+
if (filter) {
|
|
61
|
+
// Resolve annotations first, so we can filter
|
|
62
|
+
const annotations = targets.map(t => store.getAnnotation(t.annotation)!)
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.filter(filter);
|
|
65
|
+
|
|
66
|
+
return annotations[0];
|
|
67
|
+
} else {
|
|
68
|
+
const top = targets[0];
|
|
69
|
+
return top ? store.getAnnotation(top.annotation) : undefined;
|
|
70
|
+
}
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
const getIntersecting = (x: number, y: number, width: number, height: number) =>
|
|
62
|
-
tree.getIntersecting(x, y, width, height).map(target => store.getAnnotation(target.annotation) as
|
|
74
|
+
tree.getIntersecting(x, y, width, height).map(target => store.getAnnotation(target.annotation) as I);
|
|
63
75
|
|
|
64
76
|
return {
|
|
65
77
|
store: {
|
package/src/state/spatialTree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import RBush from 'rbush';
|
|
2
2
|
import { ShapeType,computeArea, intersects, isImageAnnotationTarget } from '../model';
|
|
3
3
|
import type { ImageAnnotationTarget } from '../model';
|
|
4
|
-
import type { AnnotationTarget } from '@annotorious/core';
|
|
4
|
+
import type { Annotation, AnnotationTarget, Filter } from '@annotorious/core';
|
|
5
5
|
|
|
6
6
|
interface IndexedTarget {
|
|
7
7
|
|
|
@@ -74,7 +74,8 @@ export const createSpatialTree = () => {
|
|
|
74
74
|
tree.load(indexedTargets);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
const getAt = (x: number, y: number, filter?: Filter<Annotation>): ImageAnnotationTarget[] => {
|
|
78
79
|
const idxHits = tree.search({
|
|
79
80
|
minX: x,
|
|
80
81
|
minY: y,
|
|
@@ -91,7 +92,9 @@ export const createSpatialTree = () => {
|
|
|
91
92
|
// Get smallest shape
|
|
92
93
|
if (exactHits.length > 0) {
|
|
93
94
|
exactHits.sort((a, b) => computeArea(a.selector) - computeArea(b.selector));
|
|
94
|
-
return exactHits
|
|
95
|
+
return exactHits;
|
|
96
|
+
} else {
|
|
97
|
+
return [];
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
100
|
|