@annotorious/annotorious 3.6.1 → 3.6.2
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/annotorious.es.js +3297 -3924
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +1 -2
- package/dist/annotorious.js.map +1 -1
- package/dist/model/core/polyline/polylineUtils.d.ts +2 -2
- package/dist/model/w3c/svg/pathParser.d.ts +2 -1
- package/package.json +2 -3
- package/src/model/core/polyline/polylineUtils.ts +8 -8
- package/src/model/w3c/svg/SVGSelector.ts +32 -4
- package/src/model/w3c/svg/pathParser.ts +132 -40
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { PolylineGeometry } from './Polyline';
|
|
2
|
-
export declare const approximateAsPolygon: (
|
|
1
|
+
import { PolylineGeometry, PolylinePoint } from './Polyline';
|
|
2
|
+
export declare const approximateAsPolygon: (corners: PolylinePoint[], closed?: boolean) => [number, number][];
|
|
3
3
|
export declare const computeSVGPath: (geom: PolylineGeometry) => string;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { MultiPolygonElement } from '../../core';
|
|
1
|
+
import { MultiPolygonElement, PolylineGeometry } from '../../core';
|
|
2
2
|
export declare const svgPathToMultiPolygonElement: (d: string) => MultiPolygonElement | undefined;
|
|
3
|
+
export declare const svgPathToPolyline: (d: string) => PolylineGeometry;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.2",
|
|
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",
|
|
@@ -49,11 +49,10 @@
|
|
|
49
49
|
"vitest": "^3.2.4"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@annotorious/core": "3.6.
|
|
52
|
+
"@annotorious/core": "3.6.2",
|
|
53
53
|
"dequal": "^2.0.3",
|
|
54
54
|
"rbush": "^4.0.1",
|
|
55
55
|
"simplify-js": "^1.2.4",
|
|
56
|
-
"svg-pathdata": "^8.0.0",
|
|
57
56
|
"uuid": "^11.1.0"
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ShapeType } from '../Shape';
|
|
2
2
|
import { computePolygonArea, isPointInPolygon, registerShapeUtil, type ShapeUtil } from '../shapeUtils';
|
|
3
|
-
import type { Polyline, PolylineGeometry } from './Polyline';
|
|
3
|
+
import type { Polyline, PolylineGeometry, PolylinePoint } from './Polyline';
|
|
4
4
|
|
|
5
5
|
const PolylineUtil: ShapeUtil<Polyline> = {
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ const PolylineUtil: ShapeUtil<Polyline> = {
|
|
|
10
10
|
if (!geom.closed || geom.points.length < 3)
|
|
11
11
|
return 0;
|
|
12
12
|
|
|
13
|
-
const points = approximateAsPolygon(geom);
|
|
13
|
+
const points = approximateAsPolygon(geom.points, geom.closed);
|
|
14
14
|
return computePolygonArea(points);
|
|
15
15
|
},
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ const PolylineUtil: ShapeUtil<Polyline> = {
|
|
|
18
18
|
const geom = polyline.geometry;
|
|
19
19
|
|
|
20
20
|
if (geom.closed) {
|
|
21
|
-
const points = approximateAsPolygon(geom);
|
|
21
|
+
const points = approximateAsPolygon(geom.points, geom.closed);
|
|
22
22
|
return isPointInPolygon(points, x, y);
|
|
23
23
|
} else {
|
|
24
24
|
return isPointNearPath(geom, [x, y], buffer);
|
|
@@ -27,17 +27,17 @@ const PolylineUtil: ShapeUtil<Polyline> = {
|
|
|
27
27
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
export const approximateAsPolygon = (
|
|
30
|
+
export const approximateAsPolygon = (corners: PolylinePoint[], closed = false): [number, number][] => {
|
|
31
31
|
const points: [number, number][] = [];
|
|
32
32
|
|
|
33
|
-
for (let i = 0; i <
|
|
34
|
-
const currentPoint =
|
|
35
|
-
const nextPoint =
|
|
33
|
+
for (let i = 0; i < corners.length; i++) {
|
|
34
|
+
const currentPoint = corners[i];
|
|
35
|
+
const nextPoint = corners[(i + 1) % corners.length];
|
|
36
36
|
|
|
37
37
|
points.push(currentPoint.point);
|
|
38
38
|
|
|
39
39
|
// If there's a curve to the next point, approximate it
|
|
40
|
-
if (i <
|
|
40
|
+
if (i < corners.length - 1 || closed) {
|
|
41
41
|
const hasCurve = currentPoint.outHandle || nextPoint.inHandle;
|
|
42
42
|
if (hasCurve) {
|
|
43
43
|
const curvePoints = approximateBezierCurve(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { boundsFromPoints, multipolygonElementToPath, ShapeType } from '../../core';
|
|
1
|
+
import { boundsFromPoints, computeSVGPath, multipolygonElementToPath, ShapeType } from '../../core';
|
|
2
2
|
import { parseSVGXML } from './SVG';
|
|
3
|
-
import { svgPathToMultiPolygonElement } from './pathParser';
|
|
3
|
+
import { svgPathToMultiPolygonElement, svgPathToPolyline } from './pathParser';
|
|
4
4
|
import type {
|
|
5
5
|
Ellipse,
|
|
6
6
|
EllipseGeometry,
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
MultiPolygonGeometry,
|
|
11
11
|
Polygon,
|
|
12
12
|
PolygonGeometry,
|
|
13
|
+
Polyline,
|
|
14
|
+
PolylineGeometry,
|
|
13
15
|
Shape
|
|
14
16
|
} from '../../core';
|
|
15
17
|
|
|
@@ -85,7 +87,27 @@ const parseSVGLine = (value: string): Line => {
|
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const
|
|
90
|
+
const parseSVGPathToPolyline = (value: string): Polyline => {
|
|
91
|
+
const doc = parseSVGXML(value);
|
|
92
|
+
|
|
93
|
+
const path = doc.nodeName === 'path' ? doc : Array.from(doc.querySelectorAll('path'))[0];
|
|
94
|
+
const d = path?.getAttribute('d');
|
|
95
|
+
|
|
96
|
+
if (!d)
|
|
97
|
+
throw new Error('Could not parse SVG path');
|
|
98
|
+
|
|
99
|
+
const polyline = svgPathToPolyline(d);
|
|
100
|
+
|
|
101
|
+
if (!polyline)
|
|
102
|
+
throw new Error('Could not parse SVG path');
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
type: ShapeType.POLYLINE,
|
|
106
|
+
geometry: polyline
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parseSVGPathToPolygon = (value: string): Polygon | MultiPolygon => {
|
|
89
111
|
const doc = parseSVGXML(value);
|
|
90
112
|
|
|
91
113
|
const paths = doc.nodeName === 'path' ? [doc] : Array.from(doc.querySelectorAll('path'));
|
|
@@ -121,8 +143,10 @@ export const parseSVGSelector = <T extends Shape>(valueOrSelector: SVGSelector |
|
|
|
121
143
|
|
|
122
144
|
if (value.includes('<polygon points='))
|
|
123
145
|
return parseSVGPolygon(value) as unknown as T;
|
|
146
|
+
else if (value.includes('<path ') && (value.includes(' C ') || !value.includes('Z')))
|
|
147
|
+
return parseSVGPathToPolyline(value) as unknown as T;
|
|
124
148
|
else if (value.includes('<path '))
|
|
125
|
-
return
|
|
149
|
+
return parseSVGPathToPolygon(value) as unknown as T;
|
|
126
150
|
else if (value.includes('<ellipse '))
|
|
127
151
|
return parseSVGEllipse(value) as unknown as T;
|
|
128
152
|
else if (value.includes("<line "))
|
|
@@ -164,6 +188,10 @@ export const serializeSVGSelector = (shape: Shape): SVGSelector => {
|
|
|
164
188
|
value = `<svg><line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" /></svg>`;
|
|
165
189
|
break;
|
|
166
190
|
}
|
|
191
|
+
case ShapeType.POLYLINE: {
|
|
192
|
+
const d = computeSVGPath(shape.geometry as PolylineGeometry);
|
|
193
|
+
value = `<svg><path d="${d}" /></svg>`;
|
|
194
|
+
}
|
|
167
195
|
}
|
|
168
196
|
|
|
169
197
|
if (value) {
|
|
@@ -1,63 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { approximateAsPolygon, boundsFromPoints } from '../../core';
|
|
2
|
+
import type { MultiPolygonElement, MultiPolygonRing, PolylineGeometry, PolylinePoint } from '../../core';
|
|
3
3
|
|
|
4
4
|
export const svgPathToMultiPolygonElement = (d: string): MultiPolygonElement | undefined => {
|
|
5
|
-
const commands =
|
|
5
|
+
const commands = parsePathCommands(d);
|
|
6
6
|
|
|
7
7
|
const rings: MultiPolygonRing[] = [];
|
|
8
|
-
|
|
9
8
|
let currentRing: [number, number][] = [];
|
|
10
|
-
let
|
|
9
|
+
let currentPoint: [number, number] = [0, 0];
|
|
11
10
|
|
|
12
11
|
for (const cmd of commands) {
|
|
13
|
-
switch (cmd.type) {
|
|
14
|
-
case
|
|
12
|
+
switch (cmd.type.toUpperCase()) {
|
|
13
|
+
case 'M':
|
|
15
14
|
// Start a new ring
|
|
16
15
|
if (currentRing.length > 0) {
|
|
17
16
|
rings.push({ points: currentRing });
|
|
18
17
|
currentRing = [];
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
currentRing.push(startPoint);
|
|
19
|
+
currentPoint = [cmd.args[0], cmd.args[1]];
|
|
20
|
+
currentRing.push([...currentPoint]);
|
|
23
21
|
break;
|
|
24
22
|
|
|
25
|
-
case
|
|
26
|
-
|
|
23
|
+
case 'L':
|
|
24
|
+
currentPoint = [cmd.args[0], cmd.args[1]];
|
|
25
|
+
currentRing.push([...currentPoint]);
|
|
27
26
|
break;
|
|
28
27
|
|
|
29
|
-
case
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
currentRing.push([cmd.x, lastY]);
|
|
28
|
+
case 'H':
|
|
29
|
+
currentPoint = [cmd.args[0], currentPoint[1]];
|
|
30
|
+
currentRing.push([...currentPoint]);
|
|
33
31
|
break;
|
|
34
32
|
|
|
35
|
-
case
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
currentRing.push([lastX, cmd.y]);
|
|
33
|
+
case 'V':
|
|
34
|
+
currentPoint = [currentPoint[0], cmd.args[0]];
|
|
35
|
+
currentRing.push([...currentPoint]);
|
|
39
36
|
break;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
currentRing[currentRing.length - 1][1] !== startPoint[1])) {
|
|
46
|
-
currentRing.push([...startPoint]);
|
|
47
|
-
}
|
|
48
|
-
rings.push({ points: currentRing });
|
|
49
|
-
currentRing = [];
|
|
50
|
-
startPoint = null;
|
|
37
|
+
|
|
38
|
+
case 'C':
|
|
39
|
+
// For multi-polygon, we only care about the end point
|
|
40
|
+
currentPoint = [cmd.args[4], cmd.args[5]];
|
|
41
|
+
currentRing.push([...currentPoint]);
|
|
51
42
|
break;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
case SVGPathData.ARC:
|
|
60
|
-
currentRing.push([cmd.x, cmd.y]);
|
|
43
|
+
|
|
44
|
+
case 'Z':
|
|
45
|
+
// Close the current ring (no action needed since we don't track closed state here)
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
console.warn(`Unsupported SVG path command: ${cmd.type}`);
|
|
61
50
|
break;
|
|
62
51
|
}
|
|
63
52
|
}
|
|
@@ -69,5 +58,108 @@ export const svgPathToMultiPolygonElement = (d: string): MultiPolygonElement | u
|
|
|
69
58
|
const bounds = boundsFromPoints(rings[0].points);
|
|
70
59
|
return { rings, bounds };
|
|
71
60
|
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const svgPathToPolyline = (d: string): PolylineGeometry => {
|
|
64
|
+
const commands = parsePathCommands(d);
|
|
65
|
+
|
|
66
|
+
const points: PolylinePoint[] = [];
|
|
67
|
+
|
|
68
|
+
let currentPoint: [number, number] = [0, 0];
|
|
69
|
+
let closed = false;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < commands.length; i++) {
|
|
72
|
+
const cmd = commands[i];
|
|
73
|
+
|
|
74
|
+
switch (cmd.type.toUpperCase()) {
|
|
75
|
+
case 'M':
|
|
76
|
+
currentPoint = [cmd.args[0], cmd.args[1]];
|
|
77
|
+
points.push({
|
|
78
|
+
type: 'CORNER',
|
|
79
|
+
point: [...currentPoint]
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case 'L':
|
|
84
|
+
currentPoint = [cmd.args[0], cmd.args[1]];
|
|
85
|
+
points.push({
|
|
86
|
+
type: 'CORNER',
|
|
87
|
+
point: [...currentPoint]
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'C':
|
|
92
|
+
const cp1: [number, number] = [cmd.args[0], cmd.args[1]];
|
|
93
|
+
const cp2: [number, number] = [cmd.args[2], cmd.args[3]];
|
|
94
|
+
const endPoint: [number, number] = [cmd.args[4], cmd.args[5]];
|
|
95
|
+
|
|
96
|
+
// Set outHandle for the previous point if it doesn't match the point itself
|
|
97
|
+
if (points.length > 0) {
|
|
98
|
+
const prevPoint = points[points.length - 1];
|
|
99
|
+
if (cp1[0] !== prevPoint.point[0] || cp1[1] !== prevPoint.point[1]) {
|
|
100
|
+
prevPoint.type = 'CURVE';
|
|
101
|
+
prevPoint.outHandle = cp1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create the end point with inHandle if it doesn't match the point itself
|
|
106
|
+
const newPoint: PolylinePoint = {
|
|
107
|
+
type: cp2[0] !== endPoint[0] || cp2[1] !== endPoint[1] ? 'CURVE' : 'CORNER',
|
|
108
|
+
point: endPoint
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (newPoint.type === 'CURVE')
|
|
112
|
+
newPoint.inHandle = cp2;
|
|
113
|
+
|
|
114
|
+
points.push(newPoint);
|
|
115
|
+
currentPoint = endPoint;
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'Z':
|
|
119
|
+
closed = true;
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
console.warn(`Unsupported SVG path command: ${cmd.type}`);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const bounds = boundsFromPoints(approximateAsPolygon(points, closed));
|
|
72
129
|
|
|
130
|
+
return {
|
|
131
|
+
points,
|
|
132
|
+
closed,
|
|
133
|
+
bounds
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface PathCommand {
|
|
138
|
+
|
|
139
|
+
type: string;
|
|
140
|
+
|
|
141
|
+
args: number[];
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parsePathCommands = (d: string) => {
|
|
146
|
+
const commands: PathCommand[] = [];
|
|
147
|
+
const cleanPath = d.replace(/,/g, ' ').trim();
|
|
148
|
+
|
|
149
|
+
// Updated regex to include H and V commands
|
|
150
|
+
const commandRegex = /([MmLlHhVvCcZz])\s*([^MmLlHhVvCcZz]*)/g;
|
|
151
|
+
let match;
|
|
152
|
+
|
|
153
|
+
while ((match = commandRegex.exec(cleanPath)) !== null) {
|
|
154
|
+
const [, commandLetter, argsString] = match;
|
|
155
|
+
const args = argsString.trim() === '' ? [] :
|
|
156
|
+
argsString.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
157
|
+
|
|
158
|
+
commands.push({
|
|
159
|
+
type: commandLetter,
|
|
160
|
+
args
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return commands;
|
|
73
165
|
}
|