@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.
- 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 +3644 -2444
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +2 -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/dist/model/w3c/svg/SVG.d.ts +1 -0
- package/dist/model/w3c/svg/pathParser.d.ts +2 -0
- package/package.json +4 -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
- package/src/model/w3c/svg/SVG.ts +16 -0
- package/src/model/w3c/svg/SVGSelector.ts +57 -21
- package/src/model/w3c/svg/pathParser.ts +73 -0
|
@@ -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;
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.1
|
|
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.
|
|
48
|
+
"vite-plugin-dts": "^4.5.3",
|
|
49
49
|
"vitest": "^3.0.8"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@annotorious/core": "3.1
|
|
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
|
}
|
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
|
+
}
|
package/src/model/w3c/svg/SVG.ts
CHANGED
|
@@ -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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 [
|
|
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) {
|