@annotorious/annotorious 3.5.1 → 3.6.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.
@@ -7,6 +7,7 @@ export declare enum ShapeType {
7
7
  ELLIPSE = "ELLIPSE",
8
8
  MULTIPOLYGON = "MULTIPOLYGON",
9
9
  POLYGON = "POLYGON",
10
+ POLYLINE = "POLYLINE",
10
11
  RECTANGLE = "RECTANGLE",
11
12
  LINE = "LINE"
12
13
  }
@@ -2,6 +2,7 @@ export * from './ellipse';
2
2
  export * from './line';
3
3
  export * from './multipolygon';
4
4
  export * from './polygon';
5
+ export * from './polyline';
5
6
  export * from './rectangle';
6
7
  export * from './ImageAnnotation';
7
8
  export * from './Shape';
@@ -0,0 +1,16 @@
1
+ import { Bounds, Geometry, Shape } from '../Shape';
2
+ export interface Polyline extends Shape {
3
+ geometry: PolylineGeometry;
4
+ }
5
+ export interface PolylineGeometry extends Geometry {
6
+ points: PolylinePoint[];
7
+ closed?: boolean;
8
+ bounds: Bounds;
9
+ }
10
+ export interface PolylinePoint {
11
+ type: 'CORNER' | 'CURVE';
12
+ point: [number, number];
13
+ inHandle?: [number, number];
14
+ outHandle?: [number, number];
15
+ locked?: boolean;
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Polyline';
2
+ export * from './polylineUtils';
@@ -0,0 +1,3 @@
1
+ import { PolylineGeometry } from './Polyline';
2
+ export declare const approximateAsPolygon: (geom: PolylineGeometry) => [number, number][];
3
+ export declare const computeSVGPath: (geom: PolylineGeometry) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.5.1",
3
+ "version": "3.6.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",
@@ -49,7 +49,7 @@
49
49
  "vitest": "^3.2.4"
50
50
  },
51
51
  "dependencies": {
52
- "@annotorious/core": "3.5.1",
52
+ "@annotorious/core": "3.6.0",
53
53
  "dequal": "^2.0.3",
54
54
  "rbush": "^4.0.1",
55
55
  "simplify-js": "^1.2.4",
@@ -4,8 +4,8 @@
4
4
  import type { Annotation, DrawingStyleExpression, StoreChangeEvent, User } from '@annotorious/core';
5
5
  import { isImageAnnotation, ShapeType } from '../model';
6
6
  import type { ImageAnnotation, Shape} from '../model';
7
- import { getEditor as _getEditor, EditorMount } from './editors';
8
- import { Ellipse, Line, MultiPolygon, Polygon, Rectangle} from './shapes';
7
+ import { getEditor, EditorMount } from './editors';
8
+ import { Ellipse, Line, MultiPolygon, Polygon, Polyline, Rectangle} from './shapes';
9
9
  import { getTool, listDrawingTools, ToolMount } from './tools';
10
10
  import { enableResponsive } from './utils';
11
11
  import { createSVGTransform } from './Transform';
@@ -56,8 +56,6 @@
56
56
 
57
57
  let editableAnnotations: ImageAnnotation[] | undefined;
58
58
 
59
- $: isEditable = (a: ImageAnnotation) => $selection.selected.find(s => s.id === a.id && s.editable);
60
-
61
59
  $: trackSelection($selection.selected);
62
60
 
63
61
  const trackSelection = (selected: { id: string, editable?: boolean }[]) => {
@@ -128,7 +126,7 @@
128
126
  const onPointerMove = (evt: PointerEvent) => {
129
127
  const { x, y } = getSVGPoint(evt, svgEl);
130
128
 
131
- const hit = store.getAt(x, y);
129
+ const hit = store.getAt(x, y, undefined, 2);
132
130
  if (hit) {
133
131
  if ($hover !== hit.id) {
134
132
  hover.set(hit.id);
@@ -138,8 +136,13 @@
138
136
  }
139
137
  }
140
138
 
141
- // To get around lack of TypeScript support in Svelte markup
142
- const getEditor = (shape: Shape): typeof SvelteComponent => _getEditor(shape)!;
139
+ // [annotation -> editor] - note that we may not have editors available for
140
+ // all annotations, because they might rely on plugins in some cases!
141
+ $: editors = editableAnnotations ? editableAnnotations.map(annotation => ({
142
+ annotation, editor: getEditor(annotation.target.selector)!
143
+ })).filter(t => t.editor) : undefined;
144
+
145
+ $: isEditable = (a: ImageAnnotation) => editors && editors.some(t => t.annotation.id === a.id);
143
146
  </script>
144
147
 
145
148
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
@@ -180,6 +183,11 @@
180
183
  annotation={annotation}
181
184
  geom={selector.geometry}
182
185
  style={style} />
186
+ {:else if (selector?.type === ShapeType.POLYLINE)}
187
+ <Polyline
188
+ annotation={annotation}
189
+ geom={selector.geometry}
190
+ style={style} />
183
191
  {:else if (selector?.type === ShapeType.LINE)}
184
192
  <Line
185
193
  annotation={annotation}
@@ -195,21 +203,18 @@
195
203
  bind:this={drawingEl}
196
204
  class="drawing" >
197
205
  {#if drawingEl}
198
- {#if editableAnnotations}
199
- {#each editableAnnotations as editable}
200
- {@const editor = getEditor(editable.target.selector)}
201
- {#if editor}
202
- {#key editable.id}
203
- <EditorMount
204
- target={drawingEl}
205
- editor={getEditor(editable.target.selector)}
206
- annotation={editable}
207
- style={style}
208
- transform={transform}
209
- viewportScale={$scale}
210
- on:change={onChangeSelected(editable)} />
211
- {/key}
212
- {/if}
206
+ {#if editors}
207
+ {#each editors as editable}
208
+ {#key editable.annotation.id}
209
+ <EditorMount
210
+ target={drawingEl}
211
+ editor={editable.editor}
212
+ annotation={editable.annotation}
213
+ style={style}
214
+ transform={transform}
215
+ viewportScale={$scale}
216
+ on:change={onChangeSelected(editable.annotation)} />
217
+ {/key}
213
218
  {/each}
214
219
  {:else if (tool && drawingEnabled)}
215
220
  {#key `${toolName}-${toolMountKey}`}
@@ -27,7 +27,7 @@ export const addEventListeners = <T extends Annotation>(svg: SVGSVGElement, stor
27
27
  if (duration < MAX_CLICK_DURATION) {
28
28
  const { x, y } = getSVGPoint(evt, svg);
29
29
 
30
- const annotation = store.getAt(x, y) as T | undefined;
30
+ const annotation = store.getAt(x, y, undefined, 2) as T | undefined;
31
31
 
32
32
  if (annotation)
33
33
  dispatch('click', { originalEvent: evt, annotation });
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import type { DrawingStyleExpression } from '@annotorious/core';
3
+ import { computeSVGPath} from '../../model';
4
+ import type { Geometry, ImageAnnotation, PolylineGeometry } 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
+ $: d = computeSVGPath(geom as PolylineGeometry);
15
+
16
+ $: cssClass = (geom as PolylineGeometry).closed ? 'closed' : 'open'
17
+ </script>
18
+
19
+ <g class="a9s-annotation" data-id={annotation.id}>
20
+ <path
21
+ class={`a9s-outer ${cssClass}`}
22
+ style={computedStyle ? 'display:none;' : undefined}
23
+ d={d} />
24
+
25
+ <path
26
+ class={`a9s-inner ${cssClass}`}
27
+ style={computedStyle}
28
+ d={d} />
29
+ </g>
30
+
31
+ <style>
32
+ path.open {
33
+ fill: transparent !important;
34
+ }
35
+ </style>
@@ -1,5 +1,6 @@
1
1
  export { default as Ellipse } from './Ellipse.svelte';
2
+ export { default as Line } from './Line.svelte';
2
3
  export { default as MultiPolygon } from './MultiPolygon.svelte';
3
4
  export { default as Polygon } from './Polygon.svelte';
5
+ export { default as Polyline } from './Polyline.svelte';
4
6
  export { default as Rectangle } from './Rectangle.svelte';
5
- export { default as Line } from './Line.svelte';
@@ -16,6 +16,8 @@ export enum ShapeType {
16
16
 
17
17
  POLYGON = 'POLYGON',
18
18
 
19
+ POLYLINE = 'POLYLINE',
20
+
19
21
  RECTANGLE = 'RECTANGLE',
20
22
 
21
23
  LINE = 'LINE'
@@ -2,6 +2,7 @@ export * from './ellipse';
2
2
  export * from './line';
3
3
  export * from './multipolygon';
4
4
  export * from './polygon';
5
+ export * from './polyline';
5
6
  export * from './rectangle';
6
7
  export * from './ImageAnnotation';
7
8
  export * from './Shape';
@@ -0,0 +1,31 @@
1
+ import type { Bounds, Geometry, Shape } from '../Shape';
2
+
3
+ export interface Polyline extends Shape {
4
+
5
+ geometry: PolylineGeometry;
6
+
7
+ }
8
+
9
+ export interface PolylineGeometry extends Geometry {
10
+
11
+ points: PolylinePoint[];
12
+
13
+ closed?: boolean;
14
+
15
+ bounds: Bounds;
16
+
17
+ }
18
+
19
+ export interface PolylinePoint {
20
+
21
+ type: 'CORNER' | 'CURVE';
22
+
23
+ point: [number, number];
24
+
25
+ inHandle?: [number, number];
26
+
27
+ outHandle?: [number, number];
28
+
29
+ locked?: boolean;
30
+
31
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Polyline';
2
+ export * from './polylineUtils';
@@ -0,0 +1,193 @@
1
+ import { ShapeType } from '../Shape';
2
+ import { computePolygonArea, isPointInPolygon, registerShapeUtil, type ShapeUtil } from '../shapeUtils';
3
+ import type { Polyline, PolylineGeometry } from './Polyline';
4
+
5
+ const PolylineUtil: ShapeUtil<Polyline> = {
6
+
7
+ area: (polyline: Polyline): number => {
8
+ const geom = polyline.geometry;
9
+
10
+ if (!geom.closed || geom.points.length < 3)
11
+ return 0;
12
+
13
+ const points = approximateAsPolygon(geom);
14
+ return computePolygonArea(points);
15
+ },
16
+
17
+ intersects: (polyline: Polyline, x: number, y: number, buffer: number = 2): boolean => {
18
+ const geom = polyline.geometry;
19
+
20
+ if (geom.closed) {
21
+ const points = approximateAsPolygon(geom);
22
+ return isPointInPolygon(points, x, y);
23
+ } else {
24
+ return isPointNearPath(geom, [x, y], buffer);
25
+ }
26
+ }
27
+
28
+ };
29
+
30
+ export const approximateAsPolygon = (geom: PolylineGeometry): [number, number][] => {
31
+ const points: [number, number][] = [];
32
+
33
+ for (let i = 0; i < geom.points.length; i++) {
34
+ const currentPoint = geom.points[i];
35
+ const nextPoint = geom.points[(i + 1) % geom.points.length];
36
+
37
+ points.push(currentPoint.point);
38
+
39
+ // If there's a curve to the next point, approximate it
40
+ if (i < geom.points.length - 1 || geom.closed) {
41
+ const hasCurve = currentPoint.outHandle || nextPoint.inHandle;
42
+ if (hasCurve) {
43
+ const curvePoints = approximateBezierCurve(
44
+ currentPoint.point,
45
+ currentPoint.outHandle || currentPoint.point,
46
+ nextPoint.inHandle || nextPoint.point,
47
+ nextPoint.point,
48
+ 10 // number of approximation segments
49
+ );
50
+ points.push(...curvePoints.slice(1)); // Skip first point (already added)
51
+ }
52
+ }
53
+ }
54
+
55
+ return points;
56
+ }
57
+
58
+ const approximateBezierCurve = (
59
+ p0: [number, number],
60
+ p1: [number, number],
61
+ p2: [number, number],
62
+ p3: [number, number],
63
+ segments: number = 10
64
+ ): [number, number][] => {
65
+ const points: [number, number][] = [];
66
+
67
+ for (let i = 0; i <= segments; i++) {
68
+ const t = i / segments;
69
+ const x = Math.pow(1 - t, 3) * p0[0] +
70
+ 3 * Math.pow(1 - t, 2) * t * p1[0] +
71
+ 3 * (1 - t) * Math.pow(t, 2) * p2[0] +
72
+ Math.pow(t, 3) * p3[0];
73
+ const y = Math.pow(1 - t, 3) * p0[1] +
74
+ 3 * Math.pow(1 - t, 2) * t * p1[1] +
75
+ 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
76
+ Math.pow(t, 3) * p3[1];
77
+ points.push([x, y]);
78
+ }
79
+
80
+ return points;
81
+ }
82
+
83
+ const isPointNearPath = (geom: PolylineGeometry, point: [number, number], buffer: number): boolean => {
84
+ for (let i = 0; i < geom.points.length - 1; i++) {
85
+ const currentPoint = geom.points[i];
86
+ const nextPoint = geom.points[i + 1];
87
+
88
+ const hasCurve = currentPoint.outHandle || nextPoint.inHandle;
89
+ if (hasCurve) {
90
+ const curvePoints = approximateBezierCurve(
91
+ currentPoint.point,
92
+ currentPoint.outHandle || currentPoint.point,
93
+ nextPoint.inHandle || nextPoint.point,
94
+ nextPoint.point,
95
+ 20 // TODO make configurable? Based on scale factor? Length?
96
+ );
97
+
98
+ for (let j = 0; j < curvePoints.length - 1; j++) {
99
+ const distance = distanceToLineSegment(point, curvePoints[j], curvePoints[j + 1]);
100
+ if (distance <= buffer) return true;
101
+ }
102
+ } else {
103
+ const distance = distanceToLineSegment(point, currentPoint.point, nextPoint.point);
104
+ if (distance <= buffer) return true;
105
+ }
106
+ }
107
+
108
+ return false;
109
+ }
110
+
111
+ const distanceToLineSegment = (
112
+ point: [number, number],
113
+ lineStart: [number, number],
114
+ lineEnd: [number, number]
115
+ ): number => {
116
+ const [px, py] = point;
117
+ const [x1, y1] = lineStart;
118
+ const [x2, y2] = lineEnd;
119
+
120
+ const dx = x2 - x1;
121
+ const dy = y2 - y1;
122
+ const length = Math.sqrt(dx * dx + dy * dy);
123
+
124
+ if (length === 0) {
125
+ // Line segment is a point
126
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
127
+ }
128
+
129
+ // Calculate the projection parameter t to see where the perpendicular falls
130
+ const t = ((px - x1) * dx + (py - y1) * dy) / (length * length);
131
+
132
+ if (t <= 0) {
133
+ // Closest point is the start of the segment
134
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
135
+ } else if (t >= 1) {
136
+ // Closest point is the end of the segment
137
+ return Math.sqrt((px - x2) * (px - x2) + (py - y2) * (py - y2));
138
+ } else {
139
+ // Closest point is on the segment - use the exact line distance formula
140
+ // This is the same formula as your LineUtil.intersects
141
+ const area = Math.abs(((y2 - y1) * px) - ((x2 - x1) * py) + (x2 * y1) - (y2 * x1));
142
+ return area / length;
143
+ }
144
+ };
145
+
146
+ export const computeSVGPath = (geom: PolylineGeometry) => {
147
+ if (!geom.points || geom.points.length === 0)
148
+ return '';
149
+
150
+ const pathCommands: string[] = [];
151
+
152
+ const firstPoint = geom.points[0];
153
+ pathCommands.push(`M ${firstPoint.point[0]} ${firstPoint.point[1]}`);
154
+
155
+ for (let i = 1; i < geom.points.length; i++) {
156
+ const currentPoint = geom.points[i];
157
+ const previousPoint = geom.points[i - 1];
158
+
159
+ if (currentPoint.type === 'CURVE' || previousPoint.type === 'CURVE') {
160
+ // Cubic Bézier curve
161
+ const cp1 = previousPoint.outHandle || previousPoint.point;
162
+ const cp2 = currentPoint.inHandle || currentPoint.point;
163
+ const endPoint = currentPoint.point;
164
+
165
+ pathCommands.push(`C ${cp1[0]} ${cp1[1]} ${cp2[0]} ${cp2[1]} ${endPoint[0]} ${endPoint[1]}`);
166
+ } else {
167
+ // Straight line
168
+ pathCommands.push(`L ${currentPoint.point[0]} ${currentPoint.point[1]}`);
169
+ }
170
+ }
171
+
172
+ if (geom.closed) {
173
+ // Handle curve from last point back to first point
174
+ const lastPoint = geom.points[geom.points.length - 1];
175
+ const firstPointRef = geom.points[0];
176
+
177
+ const hasClosingCurve = lastPoint.outHandle || firstPointRef.inHandle;
178
+
179
+ if (hasClosingCurve) {
180
+ const cp1 = lastPoint.outHandle || lastPoint.point;
181
+ const cp2 = firstPointRef.inHandle || firstPointRef.point;
182
+ const endPoint = firstPointRef.point;
183
+
184
+ pathCommands.push(`C ${cp1[0]} ${cp1[1]} ${cp2[0]} ${cp2[1]} ${endPoint[0]} ${endPoint[1]}`);
185
+ }
186
+
187
+ pathCommands.push('Z');
188
+ }
189
+
190
+ return pathCommands.join(' ');
191
+ }
192
+
193
+ registerShapeUtil(ShapeType.POLYLINE, PolylineUtil);