@annotorious/annotorious 3.1.7 → 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.
Files changed (31) hide show
  1. package/dist/annotation/editors/editorsRegistry.d.ts +1 -1
  2. package/dist/annotation/editors/multipolygon/MultiPolygonEditor.svelte.d.ts +1 -0
  3. package/dist/annotation/editors/multipolygon/index.d.ts +1 -0
  4. package/dist/annotation/shapes/MultiPolygon.svelte.d.ts +1 -0
  5. package/dist/annotation/shapes/index.d.ts +1 -0
  6. package/dist/annotorious.css +1 -1
  7. package/dist/annotorious.es.js +2150 -1547
  8. package/dist/annotorious.es.js.map +1 -1
  9. package/dist/annotorious.js +1 -1
  10. package/dist/annotorious.js.map +1 -1
  11. package/dist/model/core/Shape.d.ts +1 -0
  12. package/dist/model/core/index.d.ts +1 -0
  13. package/dist/model/core/multipolygon/MultiPolygon.d.ts +15 -0
  14. package/dist/model/core/multipolygon/index.d.ts +2 -0
  15. package/dist/model/core/multipolygon/multiPolygonUtils.d.ts +3 -0
  16. package/dist/model/core/shapeUtils.d.ts +3 -0
  17. package/package.json +3 -3
  18. package/src/Annotorious.css +1 -0
  19. package/src/annotation/SVGAnnotationLayer.svelte +6 -0
  20. package/src/annotation/editors/editorsRegistry.ts +4 -2
  21. package/src/annotation/editors/multipolygon/MultiPolygonEditor.svelte +116 -0
  22. package/src/annotation/editors/multipolygon/index.ts +1 -0
  23. package/src/annotation/shapes/MultiPolygon.svelte +31 -0
  24. package/src/annotation/shapes/index.ts +1 -0
  25. package/src/model/core/Shape.ts +2 -0
  26. package/src/model/core/index.ts +1 -0
  27. package/src/model/core/multipolygon/MultiPolygon.ts +30 -0
  28. package/src/model/core/multipolygon/index.ts +2 -0
  29. package/src/model/core/multipolygon/multiPolygonUtils.ts +63 -0
  30. package/src/model/core/polygon/polygonUtils.ts +6 -29
  31. package/src/model/core/shapeUtils.ts +49 -1
@@ -5,6 +5,7 @@ export interface Shape extends AbstractSelector {
5
5
  }
6
6
  export declare enum ShapeType {
7
7
  ELLIPSE = "ELLIPSE",
8
+ MULTIPOLYGLON = "MULTIPOLYGON",
8
9
  POLYGON = "POLYGON",
9
10
  RECTANGLE = "RECTANGLE"
10
11
  }
@@ -1,4 +1,5 @@
1
1
  export * from './ellipse';
2
+ export * from './multipolygon';
2
3
  export * from './polygon';
3
4
  export * from './rectangle';
4
5
  export * from './ImageAnnotation';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './MultiPolygon';
2
+ export * from './multiPolygonUtils';
@@ -0,0 +1,3 @@
1
+ import { MultiPolygonElement } from './MultiPolygon';
2
+ export declare const boundsFromMultiPolygonElements: (elements: MultiPolygonElement[]) => import('../Shape').Bounds;
3
+ export declare const multipolygonElementToPath: (element: MultiPolygonElement) => string;
@@ -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.1.7",
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.0",
48
+ "vite-plugin-dts": "^4.5.3",
49
49
  "vitest": "^3.0.8"
50
50
  },
51
51
  "dependencies": {
52
- "@annotorious/core": "3.1.7",
52
+ "@annotorious/core": "3.2.0",
53
53
  "rbush": "^4.0.1",
54
54
  "uuid": "^11.1.0"
55
55
  }
@@ -28,6 +28,7 @@
28
28
  }
29
29
 
30
30
  .a9s-annotationlayer ellipse,
31
+ .a9s-annotationlayer path,
31
32
  .a9s-annotationlayer polygon,
32
33
  .a9s-annotationlayer rect {
33
34
  fill: transparent;
@@ -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>
@@ -1,3 +1,4 @@
1
1
  export { default as Ellipse } from './Ellipse.svelte';
2
+ export { default as MultiPolygon } from './MultiPolygon.svelte';
2
3
  export { default as Polygon } from './Polygon.svelte';
3
4
  export { default as Rectangle } from './Rectangle.svelte';
@@ -12,6 +12,8 @@ export enum ShapeType {
12
12
 
13
13
  ELLIPSE = 'ELLIPSE',
14
14
 
15
+ MULTIPOLYGLON = 'MULTIPOLYGON',
16
+
15
17
  POLYGON = 'POLYGON',
16
18
 
17
19
  RECTANGLE = 'RECTANGLE'
@@ -1,4 +1,5 @@
1
1
  export * from './ellipse';
2
+ export * from './multipolygon';
2
3
  export * from './polygon';
3
4
  export * from './rectangle';
4
5
  export * from './ImageAnnotation';
@@ -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,2 @@
1
+ export * from './MultiPolygon';
2
+ export * from './multiPolygonUtils';
@@ -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 { registerShapeUtil, type ShapeUtil } from '../shapeUtils';
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 { points } = polygon.geometry;
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
- // Based on https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html
23
- const { points } = polygon.geometry;
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
+ }