@annotorious/annotorious 3.1.6 → 3.2.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/editorsRegistry.d.ts +1 -1
- package/dist/annotation/editors/multipolygon/MultiPolygonEditor.svelte.d.ts +1 -0
- package/dist/annotation/editors/multipolygon/index.d.ts +1 -0
- package/dist/annotation/shapes/MultiPolygon.svelte.d.ts +1 -0
- package/dist/annotation/shapes/index.d.ts +1 -0
- package/dist/annotorious.css +1 -1
- package/dist/annotorious.es.js +2150 -1547
- 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/Shape.d.ts +1 -0
- package/dist/model/core/index.d.ts +1 -0
- package/dist/model/core/multipolygon/MultiPolygon.d.ts +15 -0
- package/dist/model/core/multipolygon/index.d.ts +2 -0
- package/dist/model/core/multipolygon/multiPolygonUtils.d.ts +3 -0
- package/dist/model/core/shapeUtils.d.ts +3 -0
- package/package.json +3 -3
- package/src/Annotorious.css +1 -0
- package/src/annotation/SVGAnnotationLayer.svelte +6 -0
- package/src/annotation/editors/editorsRegistry.ts +4 -2
- package/src/annotation/editors/multipolygon/MultiPolygonEditor.svelte +116 -0
- package/src/annotation/editors/multipolygon/index.ts +1 -0
- package/src/annotation/shapes/MultiPolygon.svelte +31 -0
- package/src/annotation/shapes/index.ts +1 -0
- package/src/model/core/Shape.ts +2 -0
- package/src/model/core/index.ts +1 -0
- package/src/model/core/multipolygon/MultiPolygon.ts +30 -0
- package/src/model/core/multipolygon/index.ts +2 -0
- package/src/model/core/multipolygon/multiPolygonUtils.ts +63 -0
- package/src/model/core/polygon/polygonUtils.ts +6 -29
- package/src/model/core/shapeUtils.ts +49 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Bounds, Geometry, Shape } from '../Shape';
|
|
2
|
+
export interface MultiPolygon extends Shape {
|
|
3
|
+
geometry: MultiPolygonGeometry;
|
|
4
|
+
}
|
|
5
|
+
export interface MultiPolygonGeometry extends Geometry {
|
|
6
|
+
polygons: Array<MultiPolygonElement>;
|
|
7
|
+
bounds: Bounds;
|
|
8
|
+
}
|
|
9
|
+
export interface MultiPolygonElement {
|
|
10
|
+
rings: Array<MultiPolygonRing>;
|
|
11
|
+
bounds: Bounds;
|
|
12
|
+
}
|
|
13
|
+
export interface MultiPolygonRing {
|
|
14
|
+
points: Array<[number, number]>;
|
|
15
|
+
}
|
|
@@ -29,3 +29,6 @@ export declare const intersects: (shape: Shape, x: number, y: number) => boolean
|
|
|
29
29
|
* @returns the Bounds
|
|
30
30
|
*/
|
|
31
31
|
export declare const boundsFromPoints: (points: Array<[number, number]>) => Bounds;
|
|
32
|
+
export declare const computePolygonArea: (points: [number, number][]) => number;
|
|
33
|
+
export declare const isPointInPolygon: (points: [number, number][], x: number, y: number) => boolean;
|
|
34
|
+
export declare const pointsToPath: (points: [number, number][], close?: boolean) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"svelte-preprocess": "^6.0.3",
|
|
46
46
|
"typescript": "5.8.2",
|
|
47
47
|
"vite": "^5.4.14",
|
|
48
|
-
"vite-plugin-dts": "^4.5.
|
|
48
|
+
"vite-plugin-dts": "^4.5.3",
|
|
49
49
|
"vitest": "^3.0.8"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@annotorious/core": "3.
|
|
52
|
+
"@annotorious/core": "3.2.0",
|
|
53
53
|
"rbush": "^4.0.1",
|
|
54
54
|
"uuid": "^11.1.0"
|
|
55
55
|
}
|
package/src/Annotorious.css
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { addEventListeners, getSVGPoint } from './SVGAnnotationLayerPointerEvent';
|
|
13
13
|
import type { SvelteImageAnnotatorState } from 'src/state';
|
|
14
14
|
import type { DrawingMode } from 'src/AnnotoriousOpts';
|
|
15
|
+
import MultiPolygon from './shapes/MultiPolygon.svelte';
|
|
15
16
|
|
|
16
17
|
/** Props **/
|
|
17
18
|
export let drawingEnabled: boolean;
|
|
@@ -172,6 +173,11 @@
|
|
|
172
173
|
annotation={annotation}
|
|
173
174
|
geom={selector.geometry}
|
|
174
175
|
style={style} />
|
|
176
|
+
{:else if (selector?.type === ShapeType.MULTIPOLYGLON)}
|
|
177
|
+
<MultiPolygon
|
|
178
|
+
annotation={annotation}
|
|
179
|
+
geom={selector.geometry}
|
|
180
|
+
style={style} />
|
|
175
181
|
{/if}
|
|
176
182
|
{/key}
|
|
177
183
|
{/if}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ShapeType, type Shape } from '../../model';
|
|
2
1
|
import type { SvelteComponent } from 'svelte';
|
|
2
|
+
import { ShapeType, type Shape } from '../../model';
|
|
3
|
+
import { MultiPolygonEditor } from './multipolygon';
|
|
3
4
|
import { PolygonEditor } from './polygon';
|
|
4
5
|
import { RectangleEditor } from './rectangle';
|
|
5
6
|
|
|
6
7
|
const REGISTERED = new Map<ShapeType, typeof SvelteComponent>([
|
|
7
8
|
[ShapeType.RECTANGLE, RectangleEditor as typeof SvelteComponent],
|
|
8
|
-
[ShapeType.POLYGON, PolygonEditor as typeof SvelteComponent]
|
|
9
|
+
[ShapeType.POLYGON, PolygonEditor as typeof SvelteComponent],
|
|
10
|
+
[ShapeType.MULTIPOLYGLON, MultiPolygonEditor as typeof SvelteComponent]
|
|
9
11
|
]);
|
|
10
12
|
|
|
11
13
|
export const getEditor = (shape: Shape) => REGISTERED.get(shape.type);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
MultiPolygon,
|
|
4
|
+
MultiPolygonElement,
|
|
5
|
+
MultiPolygonGeometry,
|
|
6
|
+
Shape
|
|
7
|
+
} from '../../../model';
|
|
8
|
+
import {
|
|
9
|
+
boundsFromMultiPolygonElements,
|
|
10
|
+
boundsFromPoints,
|
|
11
|
+
multipolygonElementToPath
|
|
12
|
+
} from '../../../model';
|
|
13
|
+
import type { Transform } from '../../Transform';
|
|
14
|
+
import { Editor, Handle } from '..';
|
|
15
|
+
|
|
16
|
+
/** Props */
|
|
17
|
+
export let shape: MultiPolygon;
|
|
18
|
+
export let computedStyle: string | undefined;
|
|
19
|
+
export let transform: Transform;
|
|
20
|
+
export let viewportScale: number = 1;
|
|
21
|
+
|
|
22
|
+
$: geom = shape.geometry;
|
|
23
|
+
|
|
24
|
+
const editor = (shape: Shape, handle: string, delta: [number, number]) => {
|
|
25
|
+
const elements = ((shape.geometry) as MultiPolygonGeometry).polygons;
|
|
26
|
+
|
|
27
|
+
let updated: MultiPolygonElement[];
|
|
28
|
+
|
|
29
|
+
if (handle === 'SHAPE') {
|
|
30
|
+
updated = elements.map(element => {
|
|
31
|
+
const rings = element.rings.map((ring, r) => {
|
|
32
|
+
const points = ring.points.map((point, p) => {
|
|
33
|
+
return [point[0] + delta[0], point[1] + delta[1]];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return { points };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const bounds = boundsFromPoints(rings[0].points as [number, number][]);
|
|
40
|
+
return { rings, bounds } as MultiPolygonElement;
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
const [_, elementIdx, ringIdx, pointIdx] = handle.split('-').map(str => parseInt(str));
|
|
44
|
+
|
|
45
|
+
updated = elements.map((element, e) => {
|
|
46
|
+
if (e === elementIdx) {
|
|
47
|
+
const rings = element.rings.map((ring, r) => {
|
|
48
|
+
if (r === ringIdx) {
|
|
49
|
+
const points = ring.points.map((point, p) => {
|
|
50
|
+
if (p === pointIdx) {
|
|
51
|
+
return [point[0] + delta[0], point[1] + delta[1]];
|
|
52
|
+
} else {
|
|
53
|
+
return point;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { points };
|
|
58
|
+
} else {
|
|
59
|
+
return ring;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const bounds = boundsFromPoints(rings[0].points as [number, number][]);
|
|
64
|
+
return { rings, bounds } as MultiPolygonElement;
|
|
65
|
+
} else {
|
|
66
|
+
return element;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...shape,
|
|
73
|
+
geometry: {
|
|
74
|
+
polygons: updated,
|
|
75
|
+
bounds: boundsFromMultiPolygonElements(updated)
|
|
76
|
+
}
|
|
77
|
+
} as MultiPolygon;
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<Editor
|
|
82
|
+
shape={shape}
|
|
83
|
+
transform={transform}
|
|
84
|
+
editor={editor}
|
|
85
|
+
on:change
|
|
86
|
+
on:grab
|
|
87
|
+
on:release
|
|
88
|
+
let:grab={grab}>
|
|
89
|
+
|
|
90
|
+
{#each geom.polygons as element, elementIdx}
|
|
91
|
+
<g>
|
|
92
|
+
<path
|
|
93
|
+
class="a9s-outer"
|
|
94
|
+
style={computedStyle ? 'display:none;' : undefined}
|
|
95
|
+
fill-rule="evenodd"
|
|
96
|
+
on:pointerdown={grab('SHAPE')}
|
|
97
|
+
d={multipolygonElementToPath(element)} />
|
|
98
|
+
|
|
99
|
+
<path
|
|
100
|
+
class="a9s-inner"
|
|
101
|
+
style={computedStyle}
|
|
102
|
+
fill-rule="evenodd"
|
|
103
|
+
on:pointerdown={grab('SHAPE')}
|
|
104
|
+
d={multipolygonElementToPath(element)} />
|
|
105
|
+
|
|
106
|
+
{#each element.rings as ring, ringIdx}
|
|
107
|
+
{#each ring.points as point, pointIdx}
|
|
108
|
+
<Handle
|
|
109
|
+
on:pointerdown={grab(`HANDLE-${elementIdx}-${ringIdx}-${pointIdx}`)}
|
|
110
|
+
x={point[0]} y={point[1]}
|
|
111
|
+
scale={viewportScale} />
|
|
112
|
+
{/each}
|
|
113
|
+
{/each}
|
|
114
|
+
</g>
|
|
115
|
+
{/each}
|
|
116
|
+
</Editor>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as MultiPolygonEditor } from './MultiPolygonEditor.svelte';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DrawingStyleExpression } from '@annotorious/core';
|
|
3
|
+
import { multipolygonElementToPath } from '../../model';
|
|
4
|
+
import type { Geometry, ImageAnnotation, MultiPolygonGeometry } from '../../model';
|
|
5
|
+
import { computeStyle } from '../utils/styling';
|
|
6
|
+
|
|
7
|
+
/** Props **/
|
|
8
|
+
export let annotation: ImageAnnotation;
|
|
9
|
+
export let geom: Geometry;
|
|
10
|
+
export let style: DrawingStyleExpression<ImageAnnotation> | undefined;
|
|
11
|
+
|
|
12
|
+
$: computedStyle = computeStyle(annotation, style);
|
|
13
|
+
|
|
14
|
+
const { polygons } = geom as MultiPolygonGeometry;
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<g class="a9s-annotation" data-id={annotation.id}>
|
|
18
|
+
{#each polygons as polygonElement}
|
|
19
|
+
<path
|
|
20
|
+
class="a9s-outer"
|
|
21
|
+
style={computedStyle ? 'display:none;' : undefined}
|
|
22
|
+
fill-rule="evenodd"
|
|
23
|
+
d={multipolygonElementToPath(polygonElement)} />
|
|
24
|
+
|
|
25
|
+
<path
|
|
26
|
+
class="a9s-inner"
|
|
27
|
+
style={computedStyle}
|
|
28
|
+
fill-rule="evenodd"
|
|
29
|
+
d={multipolygonElementToPath(polygonElement)} />
|
|
30
|
+
{/each}
|
|
31
|
+
</g>
|
package/src/model/core/Shape.ts
CHANGED
package/src/model/core/index.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Bounds, Geometry, Shape } from '../Shape';
|
|
2
|
+
|
|
3
|
+
export interface MultiPolygon extends Shape {
|
|
4
|
+
|
|
5
|
+
geometry: MultiPolygonGeometry;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MultiPolygonGeometry extends Geometry {
|
|
10
|
+
|
|
11
|
+
// Each polygon is an array of rings–outer boundardy + holes
|
|
12
|
+
polygons: Array<MultiPolygonElement>
|
|
13
|
+
|
|
14
|
+
bounds: Bounds;
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MultiPolygonElement {
|
|
19
|
+
|
|
20
|
+
rings: Array<MultiPolygonRing>;
|
|
21
|
+
|
|
22
|
+
bounds: Bounds;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MultiPolygonRing {
|
|
27
|
+
|
|
28
|
+
points: Array<[number, number]>;
|
|
29
|
+
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ShapeType } from '../Shape';
|
|
2
|
+
import type { ShapeUtil } from '../shapeUtils';
|
|
3
|
+
import { boundsFromPoints, computePolygonArea, isPointInPolygon, pointsToPath, registerShapeUtil } from '../shapeUtils';
|
|
4
|
+
import type { MultiPolygon, MultiPolygonElement } from './MultiPolygon';
|
|
5
|
+
|
|
6
|
+
const MultiPolygonUtil: ShapeUtil<MultiPolygon> = {
|
|
7
|
+
|
|
8
|
+
area: (multiPolygon: MultiPolygon): number => {
|
|
9
|
+
const { polygons } = multiPolygon.geometry;
|
|
10
|
+
|
|
11
|
+
return polygons.reduce<number>((total, element) => {
|
|
12
|
+
const [exterior, ...holes] = element.rings;
|
|
13
|
+
|
|
14
|
+
const exteriorArea = computePolygonArea(exterior.points);
|
|
15
|
+
|
|
16
|
+
const holesArea = holes.reduce<number>((total, hole) =>
|
|
17
|
+
total + computePolygonArea(hole.points), 0);
|
|
18
|
+
|
|
19
|
+
// Add this polygon's contribution to total area
|
|
20
|
+
return total + exteriorArea - holesArea;
|
|
21
|
+
}, 0);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
intersects: (multiPolygon: MultiPolygon, x: number, y: number): boolean => {
|
|
25
|
+
const { polygons } = multiPolygon.geometry;
|
|
26
|
+
|
|
27
|
+
for (const element of polygons) {
|
|
28
|
+
const [exterior, ...holes] = element.rings;
|
|
29
|
+
|
|
30
|
+
if (isPointInPolygon(exterior.points, x, y)) {
|
|
31
|
+
let insideAnyHole = false;
|
|
32
|
+
|
|
33
|
+
for (const hole of holes) {
|
|
34
|
+
if (isPointInPolygon(hole.points, x, y)) {
|
|
35
|
+
insideAnyHole = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!insideAnyHole) return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const boundsFromMultiPolygonElements = (elements: MultiPolygonElement[]) => {
|
|
50
|
+
// All outer points, i.e. the points of each outer ring for each element
|
|
51
|
+
const outerPoints = elements.reduce<[number, number][]>((points, element) => {
|
|
52
|
+
return [...points, ...element.rings[0].points];
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return boundsFromPoints(outerPoints);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const multipolygonElementToPath = (element: MultiPolygonElement) => {
|
|
59
|
+
const paths = element.rings.map(ring => pointsToPath(ring.points));
|
|
60
|
+
return paths.join(' ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registerShapeUtil(ShapeType.MULTIPOLYGLON, MultiPolygonUtil);
|
|
@@ -1,41 +1,18 @@
|
|
|
1
1
|
import { ShapeType } from '../Shape';
|
|
2
|
-
import {
|
|
2
|
+
import type { ShapeUtil } from '../shapeUtils';
|
|
3
|
+
import { computePolygonArea, isPointInPolygon, registerShapeUtil } from '../shapeUtils';
|
|
3
4
|
import type { Polygon } from './Polygon';
|
|
4
5
|
|
|
5
6
|
const PolygonUtil: ShapeUtil<Polygon> = {
|
|
6
7
|
|
|
7
8
|
area: (polygon: Polygon): number => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
let area = 0;
|
|
11
|
-
let j = points.length - 1;
|
|
12
|
-
|
|
13
|
-
for (let i = 0; i < points.length; i++) {
|
|
14
|
-
area += (points[j][0] + points[i][0]) * (points[j][1] - points[i][1]);
|
|
15
|
-
j = i;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return Math.abs(0.5 * area);
|
|
9
|
+
const points = polygon.geometry.points as [number, number][];
|
|
10
|
+
return computePolygonArea(points);
|
|
19
11
|
},
|
|
20
12
|
|
|
21
13
|
intersects: (polygon: Polygon, x: number, y: number): boolean => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let inside = false;
|
|
26
|
-
|
|
27
|
-
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
28
|
-
const xi = points[i][0],
|
|
29
|
-
yi = points[i][1];
|
|
30
|
-
const xj = points[j][0],
|
|
31
|
-
yj = points[j][1];
|
|
32
|
-
|
|
33
|
-
const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
34
|
-
|
|
35
|
-
if (intersect) inside = !inside;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return inside;
|
|
14
|
+
const points = polygon.geometry.points as [number, number][];
|
|
15
|
+
return isPointInPolygon(points, x, y);
|
|
39
16
|
}
|
|
40
17
|
|
|
41
18
|
};
|
|
@@ -54,4 +54,52 @@ export const boundsFromPoints = (points: Array<[number, number]>): Bounds => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
return { minX, minY, maxX, maxY };
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const computePolygonArea = (points: [number, number][]) => {
|
|
60
|
+
let area = 0;
|
|
61
|
+
let j = points.length - 1;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < points.length; i++) {
|
|
64
|
+
area += (points[j][0] + points[i][0]) * (points[j][1] - points[i][1]);
|
|
65
|
+
j = i;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Math.abs(0.5 * area);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const isPointInPolygon = (points: [number, number][], x: number, y: number): boolean => {
|
|
72
|
+
// Based on https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html
|
|
73
|
+
let inside = false;
|
|
74
|
+
|
|
75
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
76
|
+
const xi = points[i][0],
|
|
77
|
+
yi = points[i][1];
|
|
78
|
+
const xj = points[j][0],
|
|
79
|
+
yj = points[j][1];
|
|
80
|
+
|
|
81
|
+
const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
82
|
+
|
|
83
|
+
if (intersect) inside = !inside;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return inside;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const pointsToPath = (points: [number, number][], close: boolean = true): string => {
|
|
90
|
+
let d = 'M ';
|
|
91
|
+
|
|
92
|
+
points.forEach(([x, y], idx) => {
|
|
93
|
+
if (idx === 0) {
|
|
94
|
+
// First point after the M command
|
|
95
|
+
d += `${x},${y}`;
|
|
96
|
+
} else {
|
|
97
|
+
d += ` L ${x},${y}`;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (close)
|
|
102
|
+
d += ' Z';
|
|
103
|
+
|
|
104
|
+
return d;
|
|
105
|
+
}
|