@annotorious/annotorious 3.1.7 → 3.2.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.
Files changed (36) 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 +3644 -2444
  8. package/dist/annotorious.es.js.map +1 -1
  9. package/dist/annotorious.js +2 -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/dist/model/w3c/svg/SVG.d.ts +1 -0
  18. package/dist/model/w3c/svg/pathParser.d.ts +2 -0
  19. package/package.json +4 -3
  20. package/src/Annotorious.css +1 -0
  21. package/src/annotation/SVGAnnotationLayer.svelte +6 -0
  22. package/src/annotation/editors/editorsRegistry.ts +4 -2
  23. package/src/annotation/editors/multipolygon/MultiPolygonEditor.svelte +116 -0
  24. package/src/annotation/editors/multipolygon/index.ts +1 -0
  25. package/src/annotation/shapes/MultiPolygon.svelte +31 -0
  26. package/src/annotation/shapes/index.ts +1 -0
  27. package/src/model/core/Shape.ts +2 -0
  28. package/src/model/core/index.ts +1 -0
  29. package/src/model/core/multipolygon/MultiPolygon.ts +30 -0
  30. package/src/model/core/multipolygon/index.ts +2 -0
  31. package/src/model/core/multipolygon/multiPolygonUtils.ts +63 -0
  32. package/src/model/core/polygon/polygonUtils.ts +6 -29
  33. package/src/model/core/shapeUtils.ts +49 -1
  34. package/src/model/w3c/svg/SVG.ts +16 -0
  35. package/src/model/w3c/svg/SVGSelector.ts +57 -21
  36. package/src/model/w3c/svg/pathParser.ts +73 -0
@@ -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;
@@ -2,3 +2,4 @@ export declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2
2
  export declare const sanitize: (doc: Element | Document) => Element | Document;
3
3
  /** Helper that forces an un-namespaced node to SVG **/
4
4
  export declare const insertSVGNamespace: (originalDoc: Document) => Element;
5
+ export declare const parseSVGXML: (value: string) => Element;
@@ -0,0 +1,2 @@
1
+ import { MultiPolygonElement } from '../../core';
2
+ export declare const svgPathToMultiPolygonElement: (d: string) => MultiPolygonElement | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.1.7",
3
+ "version": "3.2.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",
@@ -45,12 +45,13 @@
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.1",
53
53
  "rbush": "^4.0.1",
54
+ "svg-pathdata": "^7.1.0",
54
55
  "uuid": "^11.1.0"
55
56
  }
56
57
  }
@@ -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
+ }
@@ -33,3 +33,19 @@ export const insertSVGNamespace = (originalDoc: Document): Element => {
33
33
  const namespacedDoc = parser.parseFromString(namespaced, "image/svg+xml");
34
34
  return namespacedDoc.documentElement;
35
35
  }
36
+
37
+ export const parseSVGXML = (value: string): Element => {
38
+ const parser = new DOMParser();
39
+
40
+ const doc = parser.parseFromString(value, 'image/svg+xml');
41
+
42
+ // SVG needs a namespace declaration - check if it's set or insert if not
43
+ const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
44
+ const isDefaultNamespaceSVG = doc.lookupNamespaceURI(null); // SVG declared as default namespace
45
+
46
+ if (isPrefixDeclared || isDefaultNamespaceSVG) {
47
+ return sanitize(doc).firstChild as Element;
48
+ } else {
49
+ return sanitize(insertSVGNamespace(doc)).firstChild as Element;
50
+ }
51
+ }
@@ -1,6 +1,15 @@
1
- import type { Ellipse, EllipseGeometry, Polygon, PolygonGeometry, Shape } from '../../core';
2
- import { boundsFromPoints, ShapeType } from '../../core';
3
- import { insertSVGNamespace, sanitize, SVG_NAMESPACE } from './SVG';
1
+ import { boundsFromPoints, multipolygonElementToPath, ShapeType } from '../../core';
2
+ import { parseSVGXML } from './SVG';
3
+ import { svgPathToMultiPolygonElement } from './pathParser';
4
+ import type {
5
+ Ellipse,
6
+ EllipseGeometry,
7
+ MultiPolygon,
8
+ MultiPolygonGeometry,
9
+ Polygon,
10
+ PolygonGeometry,
11
+ Shape
12
+ } from '../../core';
4
13
 
5
14
  export interface SVGSelector {
6
15
 
@@ -10,24 +19,8 @@ export interface SVGSelector {
10
19
 
11
20
  }
12
21
 
13
- const parseSVGXML = (value: string): Element => {
14
- const parser = new DOMParser();
15
-
16
- const doc = parser.parseFromString(value, "image/svg+xml");
17
-
18
- // SVG needs a namespace declaration - check if it's set or insert if not
19
- const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
20
- const isDefaultNamespaceSVG = doc.lookupNamespaceURI(null); // SVG declared as default namespace
21
-
22
- if (isPrefixDeclared || isDefaultNamespaceSVG) {
23
- return sanitize(doc).firstChild as Element;
24
- } else {
25
- return sanitize(insertSVGNamespace(doc)).firstChild as Element;
26
- }
27
- }
28
-
29
22
  const parseSVGPolygon = (value: string): Polygon => {
30
- const [a, b, str] = value.match(/(<polygon points=["|'])([^("|')]*)/) || [];
23
+ const [_, __, str] = value.match(/(<polygon points=["|'])([^("|')]*)/) || [];
31
24
  const points = str.split(' ').map((p) => p.split(',').map(parseFloat));
32
25
 
33
26
  return {
@@ -66,17 +59,57 @@ const parseSVGEllipse = (value: string): Ellipse => {
66
59
  };
67
60
  }
68
61
 
62
+ const parseSVGPath = (value: string): Polygon | MultiPolygon => {
63
+ const doc = parseSVGXML(value);
64
+
65
+ const paths = doc.nodeName === 'path' ? [doc] : Array.from(doc.querySelectorAll('path'));
66
+ const d = paths.map(path => path.getAttribute('d') || '');
67
+
68
+ const polygons = d.map(d => svgPathToMultiPolygonElement(d)!).filter(Boolean);
69
+
70
+ const outerPoints = polygons.reduce<[number, number][]>((points, element) => {
71
+ return [...points, ...element.rings[0].points]
72
+ }, []);
73
+
74
+ const bounds = boundsFromPoints(outerPoints);
75
+
76
+ // No need to create a MultiPolygon if theres only a single element with an outer ring
77
+ const isSinglePolygon = polygons.length === 1 && polygons[0].rings.length === 1;
78
+ return isSinglePolygon ? {
79
+ type: ShapeType.POLYGON,
80
+ geometry: {
81
+ points: outerPoints,
82
+ bounds
83
+ }
84
+ } : {
85
+ type: ShapeType.MULTIPOLYGLON,
86
+ geometry: {
87
+ polygons,
88
+ bounds
89
+ }
90
+ }
91
+ }
92
+
69
93
  export const parseSVGSelector = <T extends Shape>(valueOrSelector: SVGSelector | string): T => {
70
94
  const value = typeof valueOrSelector === 'string' ? valueOrSelector : valueOrSelector.value;
71
95
 
72
96
  if (value.includes('<polygon points='))
73
97
  return parseSVGPolygon(value) as unknown as T;
98
+ else if (value.includes('<path '))
99
+ return parseSVGPath(value) as unknown as T;
74
100
  else if (value.includes('<ellipse '))
75
101
  return parseSVGEllipse(value) as unknown as T;
76
102
  else
77
103
  throw 'Unsupported SVG shape: ' + value;
78
104
  }
79
105
 
106
+ const serializeMultiPolygon = (geom: MultiPolygonGeometry) => {
107
+ const paths = geom.polygons.map(elem =>
108
+ `<path fill-rule="evenodd" d="${multipolygonElementToPath(elem)}" />`);
109
+
110
+ return `<g>${paths.join('')}</g>`
111
+ }
112
+
80
113
  export const serializeSVGSelector = (shape: Shape): SVGSelector => {
81
114
  let value: string | undefined;
82
115
 
@@ -86,7 +119,10 @@ export const serializeSVGSelector = (shape: Shape): SVGSelector => {
86
119
  value = `<svg><polygon points="${points.map((xy) => xy.join(',')).join(' ')}" /></svg>`;
87
120
  } else if (shape.type === ShapeType.ELLIPSE) {
88
121
  const geom = shape.geometry as EllipseGeometry;
89
- value = `<svg><ellipse cx="${geom.cx}" cy="${geom.cy}" rx="${geom.rx}" ry="${geom.ry}" /></svg>`
122
+ value = `<svg><ellipse cx="${geom.cx}" cy="${geom.cy}" rx="${geom.rx}" ry="${geom.ry}" /></svg>`;
123
+ } else if (shape.type === ShapeType.MULTIPOLYGLON) {
124
+ const geom = shape.geometry as MultiPolygonGeometry;
125
+ value = `<svg>${serializeMultiPolygon(geom)}</svg>`;
90
126
  }
91
127
 
92
128
  if (value) {