@annotorious/annotorious 3.4.0 → 3.4.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.
@@ -1,3 +1,4 @@
1
- import { MultiPolygonElement } from './MultiPolygon';
1
+ import { MultiPolygonElement, MultiPolygonGeometry } from './MultiPolygon';
2
2
  export declare const boundsFromMultiPolygonElements: (elements: MultiPolygonElement[]) => import('../Shape').Bounds;
3
3
  export declare const multipolygonElementToPath: (element: MultiPolygonElement) => string;
4
+ export declare const getAllCorners: (geom: MultiPolygonGeometry) => [number, number][];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.4.0",
3
+ "version": "3.4.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,7 +49,8 @@
49
49
  "vitest": "^3.1.4"
50
50
  },
51
51
  "dependencies": {
52
- "@annotorious/core": "3.4.0",
52
+ "@annotorious/core": "3.4.2",
53
+ "dequal": "^2.0.3",
53
54
  "rbush": "^4.0.1",
54
55
  "svg-pathdata": "^7.2.0",
55
56
  "uuid": "^11.1.0"
@@ -11,7 +11,7 @@
11
11
  top: 0;
12
12
  touch-action: none;
13
13
  width: 100%;
14
-
14
+ -webkit-tap-highlight-color: transparent;
15
15
  -webkit-user-select: none;
16
16
  -moz-user-select: none;
17
17
  -ms-user-select: none;
@@ -34,6 +34,7 @@
34
34
  fill: transparent;
35
35
  shape-rendering: geometricPrecision;
36
36
  vector-effect: non-scaling-stroke;
37
+ -webkit-tap-highlight-color: transparent;
37
38
  }
38
39
 
39
40
  .a9s-edge-handle {
@@ -21,10 +21,30 @@
21
21
  </script>
22
22
 
23
23
  {#if isTouch}
24
- <circle
25
- cx={x}
26
- cy={y}
27
- r={2 * handleRadius} />
24
+ <g class="a9s-touch-handle">
25
+ <circle
26
+ cx={x}
27
+ cy={y}
28
+ r={handleRadius * 10}
29
+ class="a9s-touch-halo"
30
+ class:touched={touched} />
31
+
32
+ <circle
33
+ cx={x}
34
+ cy={y}
35
+ r={handleRadius + 10 / scale}
36
+ class="a9s-handle-buffer"
37
+ on:pointerdown
38
+ on:pointerup
39
+ on:pointerdown={onPointerDown}
40
+ on:pointerup={onPointerUp} />
41
+
42
+ <circle
43
+ class="a9s-handle-dot"
44
+ cx={x}
45
+ cy={y}
46
+ r={handleRadius + 2 / scale} />
47
+ </g>
28
48
  {:else}
29
49
  <g class={`a9s-handle ${$$props.class || ''}`.trim()}>
30
50
  <circle
@@ -44,7 +64,7 @@
44
64
  class="a9s-handle-selected"
45
65
  cx={x}
46
66
  cy={y}
47
- r={handleRadius + (6 / scale)} />
67
+ r={handleRadius + (8 / scale)} />
48
68
  {/if}
49
69
 
50
70
  <circle
@@ -56,6 +76,17 @@
56
76
  {/if}
57
77
 
58
78
  <style>
79
+ .a9s-touch-halo {
80
+ fill: transparent;
81
+ pointer-events: none;
82
+ stroke-width: 0;
83
+ transition: fill 150ms;
84
+ }
85
+
86
+ .a9s-touch-halo.touched {
87
+ fill: rgba(255, 255, 255, 0.4);
88
+ }
89
+
59
90
  .a9s-handle-buffer {
60
91
  fill: transparent;
61
92
  }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { isTouch } from '../../utils';
2
+ import { isTouch } from '../utils';
3
3
 
4
4
  /** props **/
5
5
  export let x: number;
@@ -1,18 +1,30 @@
1
1
  <script lang="ts">
2
- import type {
3
- MultiPolygon,
4
- MultiPolygonElement,
5
- MultiPolygonGeometry,
6
- Shape
7
- } from '../../../model';
2
+ import { createEventDispatcher, onMount, tick } from 'svelte';
3
+ import { dequal } from 'dequal/lite';
4
+ import type { MultiPolygon, MultiPolygonElement, MultiPolygonGeometry, Shape } from '../../../model';
5
+ import { getMaskDimensions, isTouch } from '../../utils';
6
+ import type { Transform } from '../../Transform';
7
+ import Editor from '../Editor.svelte';
8
+ import Handle from '../Handle.svelte';
9
+ import MidpointHandle from '../MidpointHandle.svelte';
10
+ import { computeMidpoints } from './utils';
8
11
  import {
9
12
  boundsFromMultiPolygonElements,
10
13
  boundsFromPoints,
11
- multipolygonElementToPath
14
+ getAllCorners,
15
+ multipolygonElementToPath
12
16
  } from '../../../model';
13
- import type { Transform } from '../../Transform';
14
- import { getMaskDimensions } from '../../utils';
15
- import { Editor, Handle } from '..';
17
+
18
+ const dispatch = createEventDispatcher<{ change: MultiPolygon }>();
19
+
20
+ /** Time difference (milliseconds) required for registering a click/tap **/
21
+ const CLICK_THRESHOLD = 250;
22
+
23
+ /** Minimum distance (px) to shape required for midpoints to show */
24
+ const MIN_HOVER_DISTANCE = 1000;
25
+
26
+ /** Needed for the <mask> element **/
27
+ const MIDPOINT_SIZE = 4.5;
16
28
 
17
29
  /** Props */
18
30
  export let shape: MultiPolygon;
@@ -21,9 +33,120 @@
21
33
  export let viewportScale: number = 1;
22
34
  export let svgEl: SVGSVGElement;
23
35
 
36
+ /** Drawing tool layer **/
37
+ let visibleMidpoint: number | undefined;
38
+ let isHandleHovered = false;
39
+ let lastHandleClick: number | undefined;
40
+ let selectedCorners: { polygon: number, ring: number, point: number }[] = [];
41
+
24
42
  $: geom = shape.geometry;
25
43
 
44
+ // No support yet for adding or removing points in mobile!
45
+ $: midpoints = isTouch ? [] : computeMidpoints(geom, viewportScale);
46
+
47
+ /** Handle hover state **/
48
+ const onEnterHandle = () => isHandleHovered = true;
49
+ const onLeaveHandle = () => isHandleHovered = false;
50
+
51
+ /** Determine visible midpoint, if any **/
52
+ const onPointerMove = (evt: PointerEvent) => {
53
+ if (selectedCorners.length > 0) {
54
+ visibleMidpoint = undefined;
55
+ return;
56
+ }
57
+
58
+ const [px, py] = transform.elementToImage(evt.offsetX, evt.offsetY);
59
+
60
+ const getDistSq = (pt: number[]) =>
61
+ Math.pow(pt[0] - px, 2) + Math.pow(pt[1] - py, 2);
62
+
63
+ const closestCorner = getAllCorners(geom).reduce((closest, corner) =>
64
+ getDistSq(corner) < getDistSq(closest) ? corner : closest);
65
+
66
+ const closestVisibleMidpoint = midpoints
67
+ .filter(m => m.visible)
68
+ .reduce((closest, midpoint) =>
69
+ getDistSq(midpoint.point) < getDistSq(closest.point) ? midpoint : closest);
70
+
71
+ // Show midpoint of the mouse is at least within THRESHOLD distance
72
+ // of the midpoint or the closest corner. (Basically a poor man's shape buffering).
73
+ const threshold = Math.pow(MIN_HOVER_DISTANCE / viewportScale, 2);
74
+
75
+ const shouldShow =
76
+ getDistSq(closestCorner) < threshold ||
77
+ getDistSq(closestVisibleMidpoint.point) < threshold;
78
+
79
+ if (shouldShow)
80
+ visibleMidpoint = midpoints.indexOf(closestVisibleMidpoint);
81
+ else
82
+ visibleMidpoint = undefined;
83
+ }
84
+
85
+ /**
86
+ * SVG element keeps loosing focus when interacting with
87
+ * shapes–this function refocuses.
88
+ */
89
+ const reclaimFocus = () => {
90
+ if (document.activeElement !== svgEl)
91
+ svgEl.focus();
92
+ }
93
+
94
+ /**
95
+ * De-selects all corners and reclaims focus.
96
+ */
97
+ const onShapePointerUp = () => {
98
+ selectedCorners = [];
99
+ reclaimFocus();
100
+ }
101
+
102
+ /**
103
+ * Updates state, waiting for potential click.
104
+ */
105
+ const onHandlePointerDown = (evt: PointerEvent) => {
106
+ isHandleHovered = true;
107
+
108
+ evt.preventDefault();
109
+ evt.stopPropagation();
110
+
111
+ lastHandleClick = performance.now();
112
+ }
113
+
114
+ /** Selection handling logic **/
115
+ const onHandlePointerUp = (polygon: number, ring: number, point: number) => (evt: PointerEvent) => {
116
+ if (!lastHandleClick || isTouch) return;
117
+
118
+ // Drag, not click
119
+ if (performance.now() - lastHandleClick > CLICK_THRESHOLD) return;
120
+
121
+ // Shorthand
122
+ const isMatch = (other: { polygon: number, ring: number, point: number }) =>
123
+ other.polygon === polygon && other.ring === ring && other.point === point;
124
+
125
+ const isSelected = selectedCorners.some(isMatch);
126
+
127
+ if (evt.metaKey || evt.ctrlKey || evt.shiftKey) {
128
+ // Add to or remove from selection
129
+ if (isSelected)
130
+ selectedCorners = selectedCorners.filter(other => !isMatch(other));
131
+ else
132
+ selectedCorners = [...selectedCorners, { polygon, ring, point }];
133
+ } else {
134
+ if (isSelected && selectedCorners.length > 1)
135
+ // Keep selected, de-select others
136
+ selectedCorners = [{ polygon, ring, point }]
137
+ else if (isSelected)
138
+ // De-select
139
+ selectedCorners = [];
140
+ else
141
+ selectedCorners = [{ polygon, ring, point }];
142
+ }
143
+
144
+ reclaimFocus();
145
+ }
146
+
26
147
  const editor = (shape: Shape, handle: string, delta: [number, number]) => {
148
+ reclaimFocus();
149
+
27
150
  const elements = ((shape.geometry) as MultiPolygonGeometry).polygons;
28
151
 
29
152
  let updated: MultiPolygonElement[];
@@ -79,9 +202,126 @@
79
202
  } as MultiPolygon;
80
203
  }
81
204
 
82
- $: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
205
+ const onAddPoint = (midpointIdx: number) => async (evt: PointerEvent) => {
206
+ evt.stopPropagation();
83
207
 
84
- const maskId = `multipoly-mask-${Math.random().toString(36).substring(2, 12)}`;
208
+ const midpoint = midpoints[midpointIdx];
209
+
210
+ const updated = geom.polygons.map((element, elIdx) => {
211
+ if (elIdx === midpoint.elementIdx) {
212
+ const rings = element.rings.map((ring, ringIdx) => {
213
+ if (ringIdx === midpoint.ringIdx) {
214
+ const points = [
215
+ ...ring.points.slice(0, midpoint.pointIdx + 1),
216
+ midpoint.point,
217
+ ...ring.points.slice(midpoint.pointIdx + 1)
218
+ ] as [number, number][];
219
+
220
+ return { points };
221
+ } else {
222
+ return ring;
223
+ }
224
+ });
225
+
226
+ const bounds = boundsFromPoints(rings[0].points as [number, number][]);
227
+ return { rings, bounds } as MultiPolygonElement;
228
+ } else {
229
+ return element;
230
+ }
231
+ });
232
+
233
+ dispatch('change', {
234
+ ...shape,
235
+ geometry: {
236
+ polygons: updated,
237
+ bounds: boundsFromMultiPolygonElements(updated)
238
+ }
239
+ } as MultiPolygon);
240
+
241
+ await tick();
242
+
243
+ // Find the newly inserted handle and dispatch grab event
244
+ const newHandle = [...document.querySelectorAll(`.a9s-handle`)][midpointIdx + 1];
245
+ if (newHandle?.firstChild) {
246
+ const newEvent = new PointerEvent('pointerdown', {
247
+ bubbles: true,
248
+ cancelable: true,
249
+ clientX: evt.clientX,
250
+ clientY: evt.clientY,
251
+ pointerId: evt.pointerId,
252
+ pointerType: evt.pointerType,
253
+ isPrimary: evt.isPrimary,
254
+ buttons: evt.buttons
255
+ });
256
+
257
+ newHandle.firstChild.dispatchEvent(newEvent);
258
+ }
259
+ }
260
+
261
+ const onDeleteSelected = () => {
262
+ const updatedPolygons = geom.polygons.map((polygon, polygonIdx) => {
263
+ const hasSelected = selectedCorners.some(s => s.polygon === polygonIdx);
264
+
265
+ if (hasSelected) {
266
+ const updatedRings = polygon.rings.map((ring, ringIdx) => {
267
+ const hasSelected = selectedCorners.some(s => s.polygon === polygonIdx && s.ring === ringIdx);
268
+
269
+ // Rings needs 3 points min
270
+ if (hasSelected && ring.points.length > 3) {
271
+ const points = ring.points.filter((_, i) =>
272
+ !selectedCorners.some(s => s.polygon === polygonIdx && s.ring === ringIdx && s.point === i));
273
+
274
+ return { points };
275
+ } else {
276
+ // No points selected on this ring
277
+ return ring;
278
+ }
279
+ });
280
+
281
+ const bounds = boundsFromPoints(updatedRings[0].points as [number, number][]);
282
+ return { rings: updatedRings, bounds } as MultiPolygonElement;
283
+ } else {
284
+ // No points selected on this polygon
285
+ return polygon;
286
+ }
287
+ });
288
+
289
+ const hasChanged = !dequal(geom.polygons, updatedPolygons);
290
+ if (hasChanged) {
291
+ dispatch('change', {
292
+ ...shape,
293
+ geometry: {
294
+ polygons: updatedPolygons,
295
+ bounds: boundsFromMultiPolygonElements(updatedPolygons)
296
+ }
297
+ } as MultiPolygon);
298
+
299
+ selectedCorners = [];
300
+ }
301
+ }
302
+
303
+ onMount(() => {
304
+ if (isTouch) return;
305
+
306
+ const onKeydown = (evt: KeyboardEvent) => {
307
+ if (evt.key === 'Delete' || evt.key === 'Backspace') {
308
+ evt.preventDefault();
309
+ onDeleteSelected();
310
+ }
311
+ };
312
+
313
+ svgEl.addEventListener('pointermove', onPointerMove);
314
+ svgEl.addEventListener('keydown', onKeydown);
315
+
316
+ return () => {
317
+ svgEl.removeEventListener('pointermove', onPointerMove);
318
+ svgEl.removeEventListener('keydown', onKeydown);
319
+ }
320
+ });
321
+
322
+ $: mask = getMaskDimensions(geom.bounds, MIDPOINT_SIZE / viewportScale);
323
+
324
+ const maskId = `polygon-mask-${Math.random().toString(36).substring(2, 12)}`;
85
325
  </script>
86
326
 
87
327
  <Editor
@@ -96,23 +336,39 @@
96
336
  {#each geom.polygons as element, elementIdx}
97
337
  <g>
98
338
  <defs>
99
- <mask id={`${maskId}-${elementIdx}`} class="a9s-multipolygon-editor-mask">
339
+ <mask id={`${maskId}-${elementIdx}-outer`} class="a9s-multipolygon-editor-mask">
100
340
  <rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
101
- <path d={multipolygonElementToPath(element)} />
341
+ <path d={multipolygonElementToPath(element)} />
342
+
343
+ {#if (visibleMidpoint !== undefined && !isHandleHovered)}
344
+ {@const { point } = midpoints[visibleMidpoint]}
345
+ <circle cx={point[0]} cy={point[1]} r={MIDPOINT_SIZE / viewportScale} />
346
+ {/if}
102
347
  </mask>
348
+
349
+ {#if (visibleMidpoint !== undefined && !isHandleHovered)}
350
+ {@const { point } = midpoints[visibleMidpoint]}
351
+ <mask id={`${maskId}-${elementIdx}-inner`} class="a9s-multipolygon-editor-mask">
352
+ <rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
353
+ <circle cx={point[0]} cy={point[1]} r={MIDPOINT_SIZE / viewportScale} />
354
+ </mask>
355
+ {/if}
103
356
  </defs>
104
357
 
105
358
  <path
106
359
  class="a9s-outer"
107
- mask={`url(#${maskId}-${elementIdx})`}
360
+ mask={`url(#${maskId}-${elementIdx}-outer)`}
108
361
  fill-rule="evenodd"
362
+ on:pointerup={onShapePointerUp}
109
363
  on:pointerdown={grab('SHAPE')}
110
364
  d={multipolygonElementToPath(element)} />
111
365
 
112
366
  <path
113
367
  class="a9s-inner"
368
+ mask={`url(#${maskId}-${elementIdx}-inner)`}
114
369
  style={computedStyle}
115
370
  fill-rule="evenodd"
371
+ on:pointerup={onShapePointerUp}
116
372
  on:pointerdown={grab('SHAPE')}
117
373
  d={multipolygonElementToPath(element)} />
118
374
 
@@ -120,13 +376,29 @@
120
376
  {#each ring.points as point, pointIdx}
121
377
  <Handle
122
378
  class="a9s-corner-handle"
379
+ x={point[0]}
380
+ y={point[1]}
381
+ scale={viewportScale}
382
+ selected={selectedCorners.some(({ polygon, ring, point }) =>
383
+ polygon === elementIdx && ring === ringIdx && point === pointIdx)}
384
+ on:pointerenter={onEnterHandle}
385
+ on:pointerleave={onLeaveHandle}
386
+ on:pointerdown={onHandlePointerDown}
123
387
  on:pointerdown={grab(`HANDLE-${elementIdx}-${ringIdx}-${pointIdx}`)}
124
- x={point[0]} y={point[1]}
125
- scale={viewportScale} />
388
+ on:pointerup={onHandlePointerUp(elementIdx, ringIdx, pointIdx)} />
126
389
  {/each}
127
390
  {/each}
128
391
  </g>
129
392
  {/each}
393
+
394
+ {#if (visibleMidpoint !== undefined && !isHandleHovered)}
395
+ {@const { point } = midpoints[visibleMidpoint]}
396
+ <MidpointHandle
397
+ x={point[0]}
398
+ y={point[1]}
399
+ scale={viewportScale}
400
+ on:pointerdown={onAddPoint(visibleMidpoint)} />
401
+ {/if}
130
402
  </Editor>
131
403
 
132
404
  <style>
@@ -134,6 +406,7 @@
134
406
  fill: #fff;
135
407
  }
136
408
 
409
+ mask.a9s-multipolygon-editor-mask > circle,
137
410
  mask.a9s-multipolygon-editor-mask > path {
138
411
  fill: #000;
139
412
  }
@@ -0,0 +1,42 @@
1
+ import type { MultiPolygonGeometry } from '../../../model';
2
+
3
+ /** Minimum distance (px) between corners required for midpoints to show **/
4
+ const MIN_VISIBILITY_DISTANCE = 12;
5
+
6
+ export interface MultipolygonMidpoint {
7
+
8
+ point: [number, number];
9
+
10
+ visible: boolean;
11
+
12
+ elementIdx: number;
13
+
14
+ ringIdx: number;
15
+
16
+ pointIdx: number;
17
+
18
+ }
19
+
20
+ export const computeMidpoints = (geom: MultiPolygonGeometry, viewportScale: number) =>
21
+ geom.polygons.reduce<MultipolygonMidpoint[]>((all, element, elementIdx) => {
22
+ const forThisPolygon = element.rings.reduce<MultipolygonMidpoint[]>((forThisPolygon, ring, ringIdx) => {
23
+ const forThisRing: MultipolygonMidpoint[] = ring.points.map((thisPoint, pointIdx) => {
24
+ const nextPoint = pointIdx === ring.points.length - 1 ? ring.points[0] : ring.points[pointIdx + 1];
25
+
26
+ const x = (thisPoint[0] + nextPoint[0]) / 2;
27
+ const y = (thisPoint[1] + nextPoint[1]) / 2;
28
+
29
+ const dist = Math.sqrt(
30
+ Math.pow(nextPoint[0] - x, 2) + Math.pow(nextPoint[1] - y, 2));
31
+
32
+ // Don't show if the distance between the corners is too small
33
+ const visible = dist > MIN_VISIBILITY_DISTANCE / viewportScale;
34
+
35
+ return { point: [x, y], visible, elementIdx, ringIdx, pointIdx };
36
+ });
37
+
38
+ return [...forThisPolygon, ...forThisRing];
39
+ }, []);
40
+
41
+ return [...all, ...forThisPolygon];
42
+ }, []);
@@ -2,11 +2,11 @@
2
2
  import { createEventDispatcher, onMount, tick } from 'svelte';
3
3
  import { boundsFromPoints } from '../../../model';
4
4
  import type { Polygon, PolygonGeometry, Shape } from '../../../model';
5
- import { getMaskDimensions } from '../../utils';
5
+ import { getMaskDimensions, isTouch } from '../../utils';
6
6
  import type { Transform } from '../../Transform';
7
7
  import Editor from '../Editor.svelte';
8
8
  import Handle from '../Handle.svelte';
9
- import Midpoint from './MidpointHandle.svelte';
9
+ import MidpointHandle from '../MidpointHandle.svelte';
10
10
 
11
11
  const dispatch = createEventDispatcher<{ change: Polygon }>();
12
12
 
@@ -30,7 +30,6 @@
30
30
  export let svgEl: SVGSVGElement;
31
31
 
32
32
  /** Drawing tool layer **/
33
- let polygonEl: SVGGElement;
34
33
  let visibleMidpoint: number | undefined;
35
34
  let isHandleHovered = false;
36
35
  let lastHandleClick: number | undefined;
@@ -38,7 +37,8 @@
38
37
 
39
38
  $: geom = shape.geometry;
40
39
 
41
- $: midpoints = geom.points.map((thisCorner, idx) => {
40
+ // No support yet for adding or removing points in mobile!
41
+ $: midpoints = isTouch ? [] : geom.points.map((thisCorner, idx) => {
42
42
  const nextCorner = idx === geom.points.length - 1 ? geom.points[0] : geom.points[idx + 1];
43
43
 
44
44
  const x = (thisCorner[0] + nextCorner[0]) / 2;
@@ -122,7 +122,7 @@
122
122
 
123
123
  /** Selection handling logic **/
124
124
  const onHandlePointerUp = (idx: number) => (evt: PointerEvent) => {
125
- if (!lastHandleClick) return;
125
+ if (!lastHandleClick || isTouch) return;
126
126
 
127
127
  // Drag, not click
128
128
  if (performance.now() - lastHandleClick > CLICK_THRESHOLD) return;
@@ -225,6 +225,8 @@
225
225
  }
226
226
 
227
227
  onMount(() => {
228
+ if (isTouch) return;
229
+
228
230
  const onKeydown = (evt: KeyboardEvent) => {
229
231
  if (evt.key === 'Delete' || evt.key === 'Backspace') {
230
232
  evt.preventDefault();
@@ -284,7 +286,6 @@
284
286
  points={geom.points.map(xy => xy.join(',')).join(' ')} />
285
287
 
286
288
  <polygon
287
- bind:this={polygonEl}
288
289
  class="a9s-inner a9s-shape-handle"
289
290
  mask={`url(#${maskId}-inner)`}
290
291
  style={computedStyle}
@@ -309,7 +310,7 @@
309
310
 
310
311
  {#if (visibleMidpoint !== undefined && !isHandleHovered)}
311
312
  {@const { point } = midpoints[visibleMidpoint]}
312
- <Midpoint
313
+ <MidpointHandle
313
314
  x={point[0]}
314
315
  y={point[1]}
315
316
  scale={viewportScale}
@@ -1,7 +1,7 @@
1
1
  import { ShapeType } from '../Shape';
2
2
  import type { ShapeUtil } from '../shapeUtils';
3
3
  import { boundsFromPoints, computePolygonArea, isPointInPolygon, pointsToPath, registerShapeUtil } from '../shapeUtils';
4
- import type { MultiPolygon, MultiPolygonElement } from './MultiPolygon';
4
+ import type { MultiPolygon, MultiPolygonElement, MultiPolygonGeometry } from './MultiPolygon';
5
5
 
6
6
  const MultiPolygonUtil: ShapeUtil<MultiPolygon> = {
7
7
 
@@ -60,4 +60,14 @@ export const multipolygonElementToPath = (element: MultiPolygonElement) => {
60
60
  return paths.join(' ');
61
61
  }
62
62
 
63
+ export const getAllCorners = (geom: MultiPolygonGeometry) =>
64
+ geom.polygons.reduce<[number, number][]>((all, element) => (
65
+ [
66
+ ...all,
67
+ ...element.rings.reduce<[number, number][]>((onThisElement, ring) => (
68
+ [...onThisElement, ...ring.points]
69
+ ), [])
70
+ ]
71
+ ), []);
72
+
63
73
  registerShapeUtil(ShapeType.MULTIPOLYGLON, MultiPolygonUtil);