@embedpdf/utils 2.5.0 → 2.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.
Files changed (47) hide show
  1. package/dist/preact/index.cjs +1 -1
  2. package/dist/preact/index.cjs.map +1 -1
  3. package/dist/preact/index.js +834 -322
  4. package/dist/preact/index.js.map +1 -1
  5. package/dist/react/index.cjs +1 -1
  6. package/dist/react/index.cjs.map +1 -1
  7. package/dist/react/index.js +834 -322
  8. package/dist/react/index.js.map +1 -1
  9. package/dist/shared/hooks/use-drag-resize.d.ts +4 -0
  10. package/dist/shared/hooks/use-interaction-handles.d.ts +18 -2
  11. package/dist/shared/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
  12. package/dist/shared/plugin-interaction-primitives/index.d.ts +1 -0
  13. package/dist/shared/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
  14. package/dist/shared/plugin-interaction-primitives/utils.d.ts +33 -0
  15. package/dist/shared-preact/hooks/use-drag-resize.d.ts +4 -0
  16. package/dist/shared-preact/hooks/use-interaction-handles.d.ts +18 -2
  17. package/dist/shared-preact/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
  18. package/dist/shared-preact/plugin-interaction-primitives/index.d.ts +1 -0
  19. package/dist/shared-preact/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
  20. package/dist/shared-preact/plugin-interaction-primitives/utils.d.ts +33 -0
  21. package/dist/shared-react/hooks/use-drag-resize.d.ts +4 -0
  22. package/dist/shared-react/hooks/use-interaction-handles.d.ts +18 -2
  23. package/dist/shared-react/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
  24. package/dist/shared-react/plugin-interaction-primitives/index.d.ts +1 -0
  25. package/dist/shared-react/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
  26. package/dist/shared-react/plugin-interaction-primitives/utils.d.ts +33 -0
  27. package/dist/shared-svelte/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
  28. package/dist/shared-svelte/plugin-interaction-primitives/index.d.ts +1 -0
  29. package/dist/shared-svelte/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
  30. package/dist/shared-svelte/plugin-interaction-primitives/utils.d.ts +33 -0
  31. package/dist/shared-vue/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
  32. package/dist/shared-vue/plugin-interaction-primitives/index.d.ts +1 -0
  33. package/dist/shared-vue/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
  34. package/dist/shared-vue/plugin-interaction-primitives/utils.d.ts +33 -0
  35. package/dist/svelte/hooks/use-drag-resize.svelte.d.ts +1 -0
  36. package/dist/svelte/hooks/use-interaction-handles.svelte.d.ts +16 -2
  37. package/dist/svelte/index.cjs +1 -1
  38. package/dist/svelte/index.cjs.map +1 -1
  39. package/dist/svelte/index.js +680 -293
  40. package/dist/svelte/index.js.map +1 -1
  41. package/dist/vue/hooks/use-drag-resize.d.ts +9 -0
  42. package/dist/vue/hooks/use-interaction-handles.d.ts +17 -2
  43. package/dist/vue/index.cjs +1 -1
  44. package/dist/vue/index.cjs.map +1 -1
  45. package/dist/vue/index.js +716 -297
  46. package/dist/vue/index.js.map +1 -1
  47. package/package.json +3 -3
@@ -2,6 +2,7 @@ import { jsx } from "preact/jsx-runtime";
2
2
  import { getCounterRotation } from "@embedpdf/utils";
3
3
  import { Fragment } from "preact";
4
4
  import { useRef, useEffect, useCallback, useMemo } from "preact/hooks";
5
+ import { rotatePointAround, calculateRotatedRectAABB, normalizeAngle } from "@embedpdf/models";
5
6
  const dblClickProp = "onDblClick";
6
7
  function CounterRotate({ children, ...props }) {
7
8
  const { rect, rotation } = props;
@@ -49,12 +50,405 @@ function CounterRotate({ children, ...props }) {
49
50
  }
50
51
  }) });
51
52
  }
53
+ const ROTATION_HANDLE_MARGIN = 35;
54
+ const HANDLE_BASE_ANGLE = {
55
+ n: 0,
56
+ ne: 45,
57
+ e: 90,
58
+ se: 135,
59
+ s: 180,
60
+ sw: 225,
61
+ w: 270,
62
+ nw: 315
63
+ };
64
+ const SECTOR_CURSORS = [
65
+ "ns-resize",
66
+ // 0: north
67
+ "nesw-resize",
68
+ // 1: NE
69
+ "ew-resize",
70
+ // 2: east
71
+ "nwse-resize",
72
+ // 3: SE
73
+ "ns-resize",
74
+ // 4: south
75
+ "nesw-resize",
76
+ // 5: SW
77
+ "ew-resize",
78
+ // 6: west
79
+ "nwse-resize"
80
+ // 7: NW
81
+ ];
82
+ function diagonalCursor(handle, pageQuarterTurns, annotationRotation = 0) {
83
+ const pageAngle = pageQuarterTurns * 90;
84
+ const totalAngle = HANDLE_BASE_ANGLE[handle] + pageAngle + annotationRotation;
85
+ const normalized = (totalAngle % 360 + 360) % 360;
86
+ const sector = Math.round(normalized / 45) % 8;
87
+ return SECTOR_CURSORS[sector];
88
+ }
89
+ function edgeOffset(k, spacing, mode) {
90
+ const base = -k / 2;
91
+ if (mode === "center") return base;
92
+ return mode === "outside" ? base - spacing : base + spacing;
93
+ }
94
+ function describeResizeFromConfig(cfg, ui = {}) {
95
+ const {
96
+ handleSize = 8,
97
+ spacing = 1,
98
+ offsetMode = "outside",
99
+ includeSides = false,
100
+ zIndex = 3,
101
+ rotationAwareCursor = true
102
+ } = ui;
103
+ const pageQuarterTurns = (cfg.pageRotation ?? 0) % 4;
104
+ const annotationRot = cfg.annotationRotation ?? 0;
105
+ const off = (edge) => ({
106
+ [edge]: edgeOffset(handleSize, spacing, offsetMode) + "px"
107
+ });
108
+ const corners = [
109
+ ["nw", { ...off("top"), ...off("left") }],
110
+ ["ne", { ...off("top"), ...off("right") }],
111
+ ["sw", { ...off("bottom"), ...off("left") }],
112
+ ["se", { ...off("bottom"), ...off("right") }]
113
+ ];
114
+ const sides = includeSides ? [
115
+ ["n", { ...off("top"), left: `calc(50% - ${handleSize / 2}px)` }],
116
+ ["s", { ...off("bottom"), left: `calc(50% - ${handleSize / 2}px)` }],
117
+ ["w", { ...off("left"), top: `calc(50% - ${handleSize / 2}px)` }],
118
+ ["e", { ...off("right"), top: `calc(50% - ${handleSize / 2}px)` }]
119
+ ] : [];
120
+ const all = [...corners, ...sides];
121
+ return all.map(([handle, pos]) => ({
122
+ handle,
123
+ style: {
124
+ position: "absolute",
125
+ width: handleSize + "px",
126
+ height: handleSize + "px",
127
+ borderRadius: "50%",
128
+ zIndex,
129
+ cursor: rotationAwareCursor ? diagonalCursor(handle, pageQuarterTurns, annotationRot) : "default",
130
+ pointerEvents: "auto",
131
+ touchAction: "none",
132
+ ...pos
133
+ },
134
+ attrs: { "data-epdf-handle": handle }
135
+ }));
136
+ }
137
+ function describeVerticesFromConfig(cfg, ui = {}, liveVertices) {
138
+ const { vertexSize = 12, zIndex = 4 } = ui;
139
+ const rect = cfg.element;
140
+ const scale = cfg.scale ?? 1;
141
+ const verts = liveVertices ?? cfg.vertices ?? [];
142
+ return verts.map((v, i) => {
143
+ const left = (v.x - rect.origin.x) * scale - vertexSize / 2;
144
+ const top = (v.y - rect.origin.y) * scale - vertexSize / 2;
145
+ return {
146
+ handle: "nw",
147
+ // not used; kept for type
148
+ style: {
149
+ position: "absolute",
150
+ left: left + "px",
151
+ top: top + "px",
152
+ width: vertexSize + "px",
153
+ height: vertexSize + "px",
154
+ borderRadius: "50%",
155
+ cursor: "pointer",
156
+ zIndex,
157
+ pointerEvents: "auto",
158
+ touchAction: "none"
159
+ },
160
+ attrs: { "data-epdf-vertex": i }
161
+ };
162
+ });
163
+ }
164
+ function describeRotationFromConfig(cfg, ui = {}, currentAngle = 0) {
165
+ const { handleSize = 16, zIndex = 5, showConnector = true, connectorWidth = 1 } = ui;
166
+ const scale = cfg.scale ?? 1;
167
+ const rect = cfg.element;
168
+ const orbitRect = cfg.rotationElement ?? rect;
169
+ const orbitCenter = cfg.rotationCenter ?? {
170
+ x: rect.origin.x + rect.size.width / 2,
171
+ y: rect.origin.y + rect.size.height / 2
172
+ };
173
+ orbitRect.size.width * scale;
174
+ orbitRect.size.height * scale;
175
+ const centerX = (orbitCenter.x - orbitRect.origin.x) * scale;
176
+ const centerY = (orbitCenter.y - orbitRect.origin.y) * scale;
177
+ const angleRad = currentAngle * Math.PI / 180;
178
+ const margin = ui.margin ?? ROTATION_HANDLE_MARGIN;
179
+ const radius = rect.size.height * scale / 2 + margin;
180
+ const handleCenterX = centerX + radius * Math.sin(angleRad);
181
+ const handleCenterY = centerY - radius * Math.cos(angleRad);
182
+ const handleLeft = handleCenterX - handleSize / 2;
183
+ const handleTop = handleCenterY - handleSize / 2;
184
+ return {
185
+ handleStyle: {
186
+ position: "absolute",
187
+ left: handleLeft + "px",
188
+ top: handleTop + "px",
189
+ width: handleSize + "px",
190
+ height: handleSize + "px",
191
+ borderRadius: "50%",
192
+ cursor: "grab",
193
+ zIndex,
194
+ pointerEvents: "auto",
195
+ touchAction: "none"
196
+ },
197
+ connectorStyle: showConnector ? {
198
+ position: "absolute",
199
+ left: centerX - connectorWidth / 2 + "px",
200
+ top: centerY - radius + "px",
201
+ width: connectorWidth + "px",
202
+ height: radius + "px",
203
+ transformOrigin: "center bottom",
204
+ transform: `rotate(${currentAngle}deg)`,
205
+ zIndex: zIndex - 1,
206
+ pointerEvents: "none"
207
+ } : {},
208
+ radius,
209
+ attrs: { "data-epdf-rotation-handle": true }
210
+ };
211
+ }
52
212
  function getAnchor(handle) {
53
213
  return {
54
214
  x: handle.includes("e") ? "left" : handle.includes("w") ? "right" : "center",
55
215
  y: handle.includes("s") ? "top" : handle.includes("n") ? "bottom" : "center"
56
216
  };
57
217
  }
218
+ function getAnchorPoint(rect, anchor) {
219
+ const x = anchor.x === "left" ? rect.origin.x : anchor.x === "right" ? rect.origin.x + rect.size.width : rect.origin.x + rect.size.width / 2;
220
+ const y = anchor.y === "top" ? rect.origin.y : anchor.y === "bottom" ? rect.origin.y + rect.size.height : rect.origin.y + rect.size.height / 2;
221
+ return { x, y };
222
+ }
223
+ function applyResizeDelta(startRect, delta, anchor) {
224
+ let x = startRect.origin.x;
225
+ let y = startRect.origin.y;
226
+ let width = startRect.size.width;
227
+ let height = startRect.size.height;
228
+ if (anchor.x === "left") {
229
+ width += delta.x;
230
+ } else if (anchor.x === "right") {
231
+ x += delta.x;
232
+ width -= delta.x;
233
+ }
234
+ if (anchor.y === "top") {
235
+ height += delta.y;
236
+ } else if (anchor.y === "bottom") {
237
+ y += delta.y;
238
+ height -= delta.y;
239
+ }
240
+ return { origin: { x, y }, size: { width, height } };
241
+ }
242
+ function enforceAspectRatio(rect, startRect, anchor, aspectRatio) {
243
+ let { x, y } = rect.origin;
244
+ let { width, height } = rect.size;
245
+ const isEdgeHandle = anchor.x === "center" || anchor.y === "center";
246
+ if (isEdgeHandle) {
247
+ if (anchor.y === "center") {
248
+ height = width / aspectRatio;
249
+ y = startRect.origin.y + (startRect.size.height - height) / 2;
250
+ } else {
251
+ width = height * aspectRatio;
252
+ x = startRect.origin.x + (startRect.size.width - width) / 2;
253
+ }
254
+ } else {
255
+ const dw = Math.abs(width - startRect.size.width);
256
+ const dh = Math.abs(height - startRect.size.height);
257
+ const total = dw + dh;
258
+ if (total === 0) {
259
+ width = startRect.size.width;
260
+ height = startRect.size.height;
261
+ } else {
262
+ const wWeight = dw / total;
263
+ const hWeight = dh / total;
264
+ const wFromW = width;
265
+ const hFromW = width / aspectRatio;
266
+ const wFromH = height * aspectRatio;
267
+ const hFromH = height;
268
+ width = wWeight * wFromW + hWeight * wFromH;
269
+ height = wWeight * hFromW + hWeight * hFromH;
270
+ }
271
+ }
272
+ if (anchor.x === "right") {
273
+ x = startRect.origin.x + startRect.size.width - width;
274
+ }
275
+ if (anchor.y === "bottom") {
276
+ y = startRect.origin.y + startRect.size.height - height;
277
+ }
278
+ return { origin: { x, y }, size: { width, height } };
279
+ }
280
+ function clampToBounds(rect, startRect, anchor, bbox, maintainAspectRatio) {
281
+ if (!bbox) return rect;
282
+ let { x, y } = rect.origin;
283
+ let { width, height } = rect.size;
284
+ width = Math.max(1, width);
285
+ height = Math.max(1, height);
286
+ const anchorX = anchor.x === "left" ? startRect.origin.x : startRect.origin.x + startRect.size.width;
287
+ const anchorY = anchor.y === "top" ? startRect.origin.y : startRect.origin.y + startRect.size.height;
288
+ const maxW = anchor.x === "left" ? bbox.width - anchorX : anchor.x === "right" ? anchorX : Math.min(startRect.origin.x, bbox.width - startRect.origin.x - startRect.size.width) * 2 + startRect.size.width;
289
+ const maxH = anchor.y === "top" ? bbox.height - anchorY : anchor.y === "bottom" ? anchorY : Math.min(startRect.origin.y, bbox.height - startRect.origin.y - startRect.size.height) * 2 + startRect.size.height;
290
+ if (maintainAspectRatio) {
291
+ const scaleW = width > maxW ? maxW / width : 1;
292
+ const scaleH = height > maxH ? maxH / height : 1;
293
+ const scale = Math.min(scaleW, scaleH);
294
+ if (scale < 1) {
295
+ width *= scale;
296
+ height *= scale;
297
+ }
298
+ } else {
299
+ width = Math.min(width, maxW);
300
+ height = Math.min(height, maxH);
301
+ }
302
+ if (anchor.x === "left") {
303
+ x = anchorX;
304
+ } else if (anchor.x === "right") {
305
+ x = anchorX - width;
306
+ } else {
307
+ x = startRect.origin.x + (startRect.size.width - width) / 2;
308
+ }
309
+ if (anchor.y === "top") {
310
+ y = anchorY;
311
+ } else if (anchor.y === "bottom") {
312
+ y = anchorY - height;
313
+ } else {
314
+ y = startRect.origin.y + (startRect.size.height - height) / 2;
315
+ }
316
+ x = Math.max(0, Math.min(x, bbox.width - width));
317
+ y = Math.max(0, Math.min(y, bbox.height - height));
318
+ return { origin: { x, y }, size: { width, height } };
319
+ }
320
+ function reanchorRect(rect, startRect, anchor) {
321
+ let x;
322
+ let y;
323
+ if (anchor.x === "left") {
324
+ x = startRect.origin.x;
325
+ } else if (anchor.x === "right") {
326
+ x = startRect.origin.x + startRect.size.width - rect.size.width;
327
+ } else {
328
+ x = startRect.origin.x + (startRect.size.width - rect.size.width) / 2;
329
+ }
330
+ if (anchor.y === "top") {
331
+ y = startRect.origin.y;
332
+ } else if (anchor.y === "bottom") {
333
+ y = startRect.origin.y + startRect.size.height - rect.size.height;
334
+ } else {
335
+ y = startRect.origin.y + (startRect.size.height - rect.size.height) / 2;
336
+ }
337
+ return { origin: { x, y }, size: rect.size };
338
+ }
339
+ function applyConstraints(position, constraints, maintainAspectRatio, skipBoundingClamp = false) {
340
+ if (!constraints) return position;
341
+ let {
342
+ origin: { x, y },
343
+ size: { width, height }
344
+ } = position;
345
+ const minW = constraints.minWidth ?? 1;
346
+ const minH = constraints.minHeight ?? 1;
347
+ const maxW = constraints.maxWidth;
348
+ const maxH = constraints.maxHeight;
349
+ if (maintainAspectRatio && width > 0 && height > 0) {
350
+ const ratio = width / height;
351
+ if (width < minW) {
352
+ width = minW;
353
+ height = width / ratio;
354
+ }
355
+ if (height < minH) {
356
+ height = minH;
357
+ width = height * ratio;
358
+ }
359
+ if (maxW !== void 0 && width > maxW) {
360
+ width = maxW;
361
+ height = width / ratio;
362
+ }
363
+ if (maxH !== void 0 && height > maxH) {
364
+ height = maxH;
365
+ width = height * ratio;
366
+ }
367
+ } else {
368
+ width = Math.max(minW, width);
369
+ height = Math.max(minH, height);
370
+ if (maxW !== void 0) width = Math.min(maxW, width);
371
+ if (maxH !== void 0) height = Math.min(maxH, height);
372
+ }
373
+ if (constraints.boundingBox && !skipBoundingClamp) {
374
+ x = Math.max(0, Math.min(x, constraints.boundingBox.width - width));
375
+ y = Math.max(0, Math.min(y, constraints.boundingBox.height - height));
376
+ }
377
+ return { origin: { x, y }, size: { width, height } };
378
+ }
379
+ function isRectWithinRotatedBounds(rect, angleDegrees, bbox) {
380
+ const eps = 1e-6;
381
+ const aabb = calculateRotatedRectAABB(rect, angleDegrees);
382
+ return aabb.origin.x >= -eps && aabb.origin.y >= -eps && aabb.origin.x + aabb.size.width <= bbox.width + eps && aabb.origin.y + aabb.size.height <= bbox.height + eps;
383
+ }
384
+ function computeResizeStep(delta, handle, config, clampLocalBounds, skipConstraintBoundingClamp) {
385
+ const { startRect, maintainAspectRatio = false, annotationRotation = 0, constraints } = config;
386
+ const anchor = getAnchor(handle);
387
+ const aspectRatio = startRect.size.width / startRect.size.height || 1;
388
+ let rect = applyResizeDelta(startRect, delta, anchor);
389
+ if (maintainAspectRatio) {
390
+ rect = enforceAspectRatio(rect, startRect, anchor, aspectRatio);
391
+ }
392
+ if (clampLocalBounds) {
393
+ rect = clampToBounds(rect, startRect, anchor, constraints == null ? void 0 : constraints.boundingBox, maintainAspectRatio);
394
+ }
395
+ rect = applyConstraints(rect, constraints, maintainAspectRatio, skipConstraintBoundingClamp);
396
+ if (skipConstraintBoundingClamp) {
397
+ rect = reanchorRect(rect, startRect, anchor);
398
+ }
399
+ if (annotationRotation !== 0) {
400
+ const anchorPt = getAnchorPoint(startRect, anchor);
401
+ const oldCenter = {
402
+ x: startRect.origin.x + startRect.size.width / 2,
403
+ y: startRect.origin.y + startRect.size.height / 2
404
+ };
405
+ const newCenter = {
406
+ x: rect.origin.x + rect.size.width / 2,
407
+ y: rect.origin.y + rect.size.height / 2
408
+ };
409
+ const oldVisual = rotatePointAround(anchorPt, oldCenter, annotationRotation);
410
+ const newVisual = rotatePointAround(anchorPt, newCenter, annotationRotation);
411
+ rect = {
412
+ origin: {
413
+ x: rect.origin.x + (oldVisual.x - newVisual.x),
414
+ y: rect.origin.y + (oldVisual.y - newVisual.y)
415
+ },
416
+ size: rect.size
417
+ };
418
+ }
419
+ return rect;
420
+ }
421
+ function computeResizedRect(delta, handle, config) {
422
+ const { annotationRotation = 0, constraints } = config;
423
+ const bbox = constraints == null ? void 0 : constraints.boundingBox;
424
+ if (annotationRotation !== 0 && bbox) {
425
+ const target = computeResizeStep(delta, handle, config, false, true);
426
+ if (isRectWithinRotatedBounds(target, annotationRotation, bbox)) {
427
+ return target;
428
+ }
429
+ let best = computeResizeStep({ x: 0, y: 0 }, handle, config, false, true);
430
+ let low = 0;
431
+ let high = 1;
432
+ for (let i = 0; i < 20; i += 1) {
433
+ const mid = (low + high) / 2;
434
+ const trial = computeResizeStep(
435
+ { x: delta.x * mid, y: delta.y * mid },
436
+ handle,
437
+ config,
438
+ false,
439
+ true
440
+ );
441
+ if (isRectWithinRotatedBounds(trial, annotationRotation, bbox)) {
442
+ best = trial;
443
+ low = mid;
444
+ } else {
445
+ high = mid;
446
+ }
447
+ }
448
+ return best;
449
+ }
450
+ return computeResizeStep(delta, handle, config, true, false);
451
+ }
58
452
  class DragResizeController {
59
453
  constructor(config, onUpdate) {
60
454
  this.config = config;
@@ -62,29 +456,41 @@ class DragResizeController {
62
456
  this.state = "idle";
63
457
  this.startPoint = null;
64
458
  this.startElement = null;
459
+ this.startRotationElement = null;
460
+ this.gestureRotationCenter = null;
65
461
  this.activeHandle = null;
66
462
  this.currentPosition = null;
67
463
  this.activeVertexIndex = null;
68
464
  this.startVertices = [];
69
465
  this.currentVertices = [];
466
+ this.rotationCenter = null;
467
+ this.centerScreen = null;
468
+ this.initialRotation = 0;
469
+ this.lastComputedRotation = 0;
470
+ this.rotationDelta = 0;
471
+ this.rotationSnappedAngle = null;
70
472
  this.currentVertices = config.vertices || [];
71
473
  }
72
474
  updateConfig(config) {
73
475
  this.config = { ...this.config, ...config };
74
- this.currentVertices = config.vertices || [];
476
+ if (this.state !== "vertex-editing") {
477
+ this.currentVertices = config.vertices || [];
478
+ }
75
479
  }
480
+ // ---------------------------------------------------------------------------
481
+ // Gesture start
482
+ // ---------------------------------------------------------------------------
76
483
  startDrag(clientX, clientY) {
77
484
  this.state = "dragging";
78
485
  this.startPoint = { x: clientX, y: clientY };
79
486
  this.startElement = { ...this.config.element };
487
+ this.startRotationElement = this.config.rotationElement ? { ...this.config.rotationElement } : null;
80
488
  this.currentPosition = { ...this.config.element };
81
489
  this.onUpdate({
82
490
  state: "start",
83
491
  transformData: {
84
492
  type: "move",
85
- changes: {
86
- rect: this.startElement
87
- }
493
+ changes: { rect: this.startElement }
88
494
  }
89
495
  });
90
496
  }
@@ -98,9 +504,7 @@ class DragResizeController {
98
504
  state: "start",
99
505
  transformData: {
100
506
  type: "resize",
101
- changes: {
102
- rect: this.startElement
103
- },
507
+ changes: { rect: this.startElement },
104
508
  metadata: {
105
509
  handle: this.activeHandle,
106
510
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -115,45 +519,87 @@ class DragResizeController {
115
519
  this.activeVertexIndex = vertexIndex;
116
520
  this.startPoint = { x: clientX, y: clientY };
117
521
  this.startVertices = [...this.currentVertices];
522
+ this.gestureRotationCenter = this.config.rotationCenter ?? {
523
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
524
+ y: this.config.element.origin.y + this.config.element.size.height / 2
525
+ };
118
526
  this.onUpdate({
119
527
  state: "start",
120
528
  transformData: {
121
529
  type: "vertex-edit",
122
- changes: {
123
- vertices: this.startVertices
124
- },
530
+ changes: { vertices: this.startVertices },
531
+ metadata: { vertexIndex }
532
+ }
533
+ });
534
+ }
535
+ startRotation(clientX, clientY, initialRotation = 0, orbitRadiusPx) {
536
+ this.state = "rotating";
537
+ this.startPoint = { x: clientX, y: clientY };
538
+ this.startElement = { ...this.config.element };
539
+ this.rotationCenter = this.config.rotationCenter ?? {
540
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
541
+ y: this.config.element.origin.y + this.config.element.size.height / 2
542
+ };
543
+ const { scale = 1 } = this.config;
544
+ const orbitRect = this.config.rotationElement ?? this.config.element;
545
+ const sw = orbitRect.size.width * scale;
546
+ const sh = orbitRect.size.height * scale;
547
+ const radius = orbitRadiusPx ?? Math.max(sw, sh) / 2 + ROTATION_HANDLE_MARGIN;
548
+ const pageRotOffset = (this.config.pageRotation ?? 0) * 90;
549
+ const screenAngleRad = (initialRotation + pageRotOffset) * Math.PI / 180;
550
+ this.centerScreen = {
551
+ x: clientX - radius * Math.sin(screenAngleRad),
552
+ y: clientY + radius * Math.cos(screenAngleRad)
553
+ };
554
+ this.initialRotation = initialRotation;
555
+ this.lastComputedRotation = initialRotation;
556
+ this.rotationDelta = 0;
557
+ this.rotationSnappedAngle = null;
558
+ this.onUpdate({
559
+ state: "start",
560
+ transformData: {
561
+ type: "rotate",
562
+ changes: { rotation: initialRotation },
125
563
  metadata: {
126
- vertexIndex
564
+ rotationAngle: initialRotation,
565
+ rotationDelta: 0,
566
+ rotationCenter: this.rotationCenter,
567
+ isSnapped: false
127
568
  }
128
569
  }
129
570
  });
130
571
  }
131
- move(clientX, clientY) {
572
+ // ---------------------------------------------------------------------------
573
+ // Gesture move
574
+ // ---------------------------------------------------------------------------
575
+ move(clientX, clientY, buttons) {
132
576
  if (this.state === "idle" || !this.startPoint) return;
577
+ if (buttons !== void 0 && buttons === 0) {
578
+ this.end();
579
+ return;
580
+ }
133
581
  if (this.state === "dragging" && this.startElement) {
134
582
  const delta = this.calculateDelta(clientX, clientY);
135
583
  const position = this.calculateDragPosition(delta);
136
584
  this.currentPosition = position;
137
585
  this.onUpdate({
138
586
  state: "move",
139
- transformData: {
140
- type: "move",
141
- changes: {
142
- rect: position
143
- }
144
- }
587
+ transformData: { type: "move", changes: { rect: position } }
145
588
  });
146
589
  } else if (this.state === "resizing" && this.activeHandle && this.startElement) {
147
- const delta = this.calculateDelta(clientX, clientY);
148
- const position = this.calculateResizePosition(delta, this.activeHandle);
590
+ const delta = this.calculateLocalDelta(clientX, clientY);
591
+ const position = computeResizedRect(delta, this.activeHandle, {
592
+ startRect: this.startElement,
593
+ maintainAspectRatio: this.config.maintainAspectRatio,
594
+ annotationRotation: this.config.annotationRotation,
595
+ constraints: this.config.constraints
596
+ });
149
597
  this.currentPosition = position;
150
598
  this.onUpdate({
151
599
  state: "move",
152
600
  transformData: {
153
601
  type: "resize",
154
- changes: {
155
- rect: position
156
- },
602
+ changes: { rect: position },
157
603
  metadata: {
158
604
  handle: this.activeHandle,
159
605
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -167,16 +613,40 @@ class DragResizeController {
167
613
  state: "move",
168
614
  transformData: {
169
615
  type: "vertex-edit",
170
- changes: {
171
- vertices
172
- },
616
+ changes: { vertices },
617
+ metadata: { vertexIndex: this.activeVertexIndex }
618
+ }
619
+ });
620
+ } else if (this.state === "rotating" && this.rotationCenter) {
621
+ const absoluteAngle = this.calculateAngleFromMouse(clientX, clientY);
622
+ const snapResult = this.applyRotationSnapping(absoluteAngle);
623
+ const snappedAngle = normalizeAngle(snapResult.angle);
624
+ const previousAngle = this.lastComputedRotation;
625
+ const rawDelta = snappedAngle - previousAngle;
626
+ const adjustedDelta = rawDelta > 180 ? rawDelta - 360 : rawDelta < -180 ? rawDelta + 360 : rawDelta;
627
+ this.rotationDelta += adjustedDelta;
628
+ this.lastComputedRotation = snappedAngle;
629
+ this.rotationSnappedAngle = snapResult.isSnapped ? snappedAngle : null;
630
+ this.onUpdate({
631
+ state: "move",
632
+ transformData: {
633
+ type: "rotate",
634
+ changes: { rotation: snappedAngle },
173
635
  metadata: {
174
- vertexIndex: this.activeVertexIndex
636
+ rotationAngle: snappedAngle,
637
+ rotationDelta: this.rotationDelta,
638
+ rotationCenter: this.rotationCenter,
639
+ isSnapped: snapResult.isSnapped,
640
+ snappedAngle: this.rotationSnappedAngle ?? void 0,
641
+ cursorPosition: { clientX, clientY }
175
642
  }
176
643
  }
177
644
  });
178
645
  }
179
646
  }
647
+ // ---------------------------------------------------------------------------
648
+ // Gesture end / cancel
649
+ // ---------------------------------------------------------------------------
180
650
  end() {
181
651
  if (this.state === "idle") return;
182
652
  const wasState = this.state;
@@ -187,23 +657,32 @@ class DragResizeController {
187
657
  state: "end",
188
658
  transformData: {
189
659
  type: "vertex-edit",
190
- changes: {
191
- vertices: this.currentVertices
192
- },
660
+ changes: { vertices: this.currentVertices },
661
+ metadata: { vertexIndex: vertexIndex || void 0 }
662
+ }
663
+ });
664
+ } else if (wasState === "rotating") {
665
+ this.onUpdate({
666
+ state: "end",
667
+ transformData: {
668
+ type: "rotate",
669
+ changes: { rotation: this.lastComputedRotation },
193
670
  metadata: {
194
- vertexIndex: vertexIndex || void 0
671
+ rotationAngle: this.lastComputedRotation,
672
+ rotationDelta: this.rotationDelta,
673
+ rotationCenter: this.rotationCenter || void 0,
674
+ isSnapped: this.rotationSnappedAngle !== null,
675
+ snappedAngle: this.rotationSnappedAngle ?? void 0
195
676
  }
196
677
  }
197
678
  });
198
679
  } else {
199
- const finalPosition = this.getCurrentPosition();
680
+ const finalPosition = this.currentPosition || this.config.element;
200
681
  this.onUpdate({
201
682
  state: "end",
202
683
  transformData: {
203
684
  type: wasState === "dragging" ? "move" : "resize",
204
- changes: {
205
- rect: finalPosition
206
- },
685
+ changes: { rect: finalPosition },
207
686
  metadata: wasState === "dragging" ? void 0 : {
208
687
  handle: handle || void 0,
209
688
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -220,11 +699,21 @@ class DragResizeController {
220
699
  state: "end",
221
700
  transformData: {
222
701
  type: "vertex-edit",
223
- changes: {
224
- vertices: this.startVertices
225
- },
702
+ changes: { vertices: this.startVertices },
703
+ metadata: { vertexIndex: this.activeVertexIndex || void 0 }
704
+ }
705
+ });
706
+ } else if (this.state === "rotating") {
707
+ this.onUpdate({
708
+ state: "end",
709
+ transformData: {
710
+ type: "rotate",
711
+ changes: { rotation: this.initialRotation },
226
712
  metadata: {
227
- vertexIndex: this.activeVertexIndex || void 0
713
+ rotationAngle: this.initialRotation,
714
+ rotationDelta: 0,
715
+ rotationCenter: this.rotationCenter || void 0,
716
+ isSnapped: false
228
717
  }
229
718
  }
230
719
  });
@@ -233,9 +722,7 @@ class DragResizeController {
233
722
  state: "end",
234
723
  transformData: {
235
724
  type: this.state === "dragging" ? "move" : "resize",
236
- changes: {
237
- rect: this.startElement
238
- },
725
+ changes: { rect: this.startElement },
239
726
  metadata: this.state === "dragging" ? void 0 : {
240
727
  handle: this.activeHandle || void 0,
241
728
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -245,18 +732,29 @@ class DragResizeController {
245
732
  }
246
733
  this.reset();
247
734
  }
735
+ // ---------------------------------------------------------------------------
736
+ // Private: state management
737
+ // ---------------------------------------------------------------------------
248
738
  reset() {
249
739
  this.state = "idle";
250
740
  this.startPoint = null;
251
741
  this.startElement = null;
742
+ this.startRotationElement = null;
743
+ this.gestureRotationCenter = null;
252
744
  this.activeHandle = null;
253
745
  this.currentPosition = null;
254
746
  this.activeVertexIndex = null;
255
747
  this.startVertices = [];
748
+ this.rotationCenter = null;
749
+ this.centerScreen = null;
750
+ this.initialRotation = 0;
751
+ this.lastComputedRotation = 0;
752
+ this.rotationDelta = 0;
753
+ this.rotationSnappedAngle = null;
256
754
  }
257
- getCurrentPosition() {
258
- return this.currentPosition || this.config.element;
259
- }
755
+ // ---------------------------------------------------------------------------
756
+ // Private: coordinate transformation (screen → page → local)
757
+ // ---------------------------------------------------------------------------
260
758
  calculateDelta(clientX, clientY) {
261
759
  if (!this.startPoint) return { x: 0, y: 0 };
262
760
  const rawDelta = {
@@ -277,18 +775,50 @@ class DragResizeController {
277
775
  y: -sin * scaledX + cos * scaledY
278
776
  };
279
777
  }
778
+ /**
779
+ * Calculate delta projected into the annotation's local (unrotated) coordinate space.
780
+ * Used for resize and vertex-edit where mouse movement must be mapped to the
781
+ * annotation's own axes, accounting for its rotation.
782
+ */
783
+ calculateLocalDelta(clientX, clientY) {
784
+ const pageDelta = this.calculateDelta(clientX, clientY);
785
+ const { annotationRotation = 0 } = this.config;
786
+ if (annotationRotation === 0) return pageDelta;
787
+ const rad = annotationRotation * Math.PI / 180;
788
+ const cos = Math.cos(rad);
789
+ const sin = Math.sin(rad);
790
+ return {
791
+ x: cos * pageDelta.x + sin * pageDelta.y,
792
+ y: -sin * pageDelta.x + cos * pageDelta.y
793
+ };
794
+ }
795
+ // ---------------------------------------------------------------------------
796
+ // Private: vertex clamping
797
+ // ---------------------------------------------------------------------------
280
798
  clampPoint(p) {
281
799
  var _a;
282
800
  const bbox = (_a = this.config.constraints) == null ? void 0 : _a.boundingBox;
283
801
  if (!bbox) return p;
284
- return {
285
- x: Math.max(0, Math.min(p.x, bbox.width)),
286
- y: Math.max(0, Math.min(p.y, bbox.height))
802
+ const { annotationRotation = 0 } = this.config;
803
+ if (annotationRotation === 0) {
804
+ return {
805
+ x: Math.max(0, Math.min(p.x, bbox.width)),
806
+ y: Math.max(0, Math.min(p.y, bbox.height))
807
+ };
808
+ }
809
+ const center = this.gestureRotationCenter ?? this.config.rotationCenter ?? {
810
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
811
+ y: this.config.element.origin.y + this.config.element.size.height / 2
287
812
  };
813
+ const visual = rotatePointAround(p, center, annotationRotation);
814
+ const clampedX = Math.max(0, Math.min(visual.x, bbox.width));
815
+ const clampedY = Math.max(0, Math.min(visual.y, bbox.height));
816
+ if (clampedX === visual.x && clampedY === visual.y) return p;
817
+ return rotatePointAround({ x: clampedX, y: clampedY }, center, -annotationRotation);
288
818
  }
289
819
  calculateVertexPosition(clientX, clientY) {
290
820
  if (this.activeVertexIndex === null) return this.startVertices;
291
- const delta = this.calculateDelta(clientX, clientY);
821
+ const delta = this.calculateLocalDelta(clientX, clientY);
292
822
  const newVertices = [...this.startVertices];
293
823
  const currentVertex = newVertices[this.activeVertexIndex];
294
824
  const moved = {
@@ -298,6 +828,9 @@ class DragResizeController {
298
828
  newVertices[this.activeVertexIndex] = this.clampPoint(moved);
299
829
  return newVertices;
300
830
  }
831
+ // ---------------------------------------------------------------------------
832
+ // Private: drag position
833
+ // ---------------------------------------------------------------------------
301
834
  calculateDragPosition(delta) {
302
835
  if (!this.startElement) return this.config.element;
303
836
  const position = {
@@ -310,262 +843,77 @@ class DragResizeController {
310
843
  height: this.startElement.size.height
311
844
  }
312
845
  };
313
- return this.applyConstraints(position);
314
- }
315
- /**
316
- * Calculate the new rect after a resize operation.
317
- * Pipeline: applyDelta → enforceAspectRatio → clampToBounds → applyConstraints
318
- */
319
- calculateResizePosition(delta, handle) {
320
- if (!this.startElement) return this.config.element;
321
- const anchor = getAnchor(handle);
322
- const aspectRatio = this.startElement.size.width / this.startElement.size.height || 1;
323
- let rect = this.applyResizeDelta(delta, anchor);
324
- if (this.config.maintainAspectRatio) {
325
- rect = this.enforceAspectRatio(rect, anchor, aspectRatio);
326
- }
327
- rect = this.clampToBounds(rect, anchor, aspectRatio);
328
- return this.applyConstraints(rect);
329
- }
330
- /**
331
- * Apply the mouse delta to produce a raw (unconstrained) resized rect.
332
- */
333
- applyResizeDelta(delta, anchor) {
334
- const start = this.startElement;
335
- let x = start.origin.x;
336
- let y = start.origin.y;
337
- let width = start.size.width;
338
- let height = start.size.height;
339
- if (anchor.x === "left") {
340
- width += delta.x;
341
- } else if (anchor.x === "right") {
342
- x += delta.x;
343
- width -= delta.x;
344
- }
345
- if (anchor.y === "top") {
346
- height += delta.y;
347
- } else if (anchor.y === "bottom") {
348
- y += delta.y;
349
- height -= delta.y;
350
- }
351
- return { origin: { x, y }, size: { width, height } };
352
- }
353
- /**
354
- * Enforce aspect ratio while respecting the anchor.
355
- * For edge handles (center anchor on one axis), the rect expands symmetrically on that axis.
356
- * For corner handles, the anchor corner stays fixed.
357
- */
358
- enforceAspectRatio(rect, anchor, aspectRatio) {
359
- const start = this.startElement;
360
- let { x, y } = rect.origin;
361
- let { width, height } = rect.size;
362
- const isEdgeHandle = anchor.x === "center" || anchor.y === "center";
363
- if (isEdgeHandle) {
364
- if (anchor.y === "center") {
365
- height = width / aspectRatio;
366
- y = start.origin.y + (start.size.height - height) / 2;
846
+ const { annotationRotation = 0, constraints } = this.config;
847
+ const bbox = constraints == null ? void 0 : constraints.boundingBox;
848
+ if (annotationRotation !== 0 && bbox) {
849
+ let aabbW;
850
+ let aabbH;
851
+ let offsetX;
852
+ let offsetY;
853
+ if (this.startRotationElement) {
854
+ aabbW = this.startRotationElement.size.width;
855
+ aabbH = this.startRotationElement.size.height;
856
+ offsetX = this.startRotationElement.origin.x - this.startElement.origin.x;
857
+ offsetY = this.startRotationElement.origin.y - this.startElement.origin.y;
367
858
  } else {
368
- width = height * aspectRatio;
369
- x = start.origin.x + (start.size.width - width) / 2;
859
+ const rad = Math.abs(annotationRotation * Math.PI / 180);
860
+ const cos = Math.abs(Math.cos(rad));
861
+ const sin = Math.abs(Math.sin(rad));
862
+ const w = position.size.width;
863
+ const h = position.size.height;
864
+ aabbW = w * cos + h * sin;
865
+ aabbH = w * sin + h * cos;
866
+ offsetX = (w - aabbW) / 2;
867
+ offsetY = (h - aabbH) / 2;
370
868
  }
371
- } else {
372
- const dw = Math.abs(width - start.size.width);
373
- const dh = Math.abs(height - start.size.height);
374
- if (dw >= dh) {
375
- height = width / aspectRatio;
376
- } else {
377
- width = height * aspectRatio;
378
- }
379
- }
380
- if (anchor.x === "right") {
381
- x = start.origin.x + start.size.width - width;
382
- }
383
- if (anchor.y === "bottom") {
384
- y = start.origin.y + start.size.height - height;
869
+ let { x, y } = position.origin;
870
+ x = Math.max(-offsetX, Math.min(x, bbox.width - aabbW - offsetX));
871
+ y = Math.max(-offsetY, Math.min(y, bbox.height - aabbH - offsetY));
872
+ return { origin: { x, y }, size: position.size };
385
873
  }
386
- return { origin: { x, y }, size: { width, height } };
874
+ return applyConstraints(position, constraints, this.config.maintainAspectRatio ?? false);
387
875
  }
876
+ // ---------------------------------------------------------------------------
877
+ // Private: rotation
878
+ // ---------------------------------------------------------------------------
388
879
  /**
389
- * Clamp rect to bounding box while respecting anchor and aspect ratio.
880
+ * Calculate the angle from the center to a point in screen coordinates.
390
881
  */
391
- clampToBounds(rect, anchor, aspectRatio) {
392
- var _a;
393
- const bbox = (_a = this.config.constraints) == null ? void 0 : _a.boundingBox;
394
- if (!bbox) return rect;
395
- const start = this.startElement;
396
- let { x, y } = rect.origin;
397
- let { width, height } = rect.size;
398
- width = Math.max(1, width);
399
- height = Math.max(1, height);
400
- const anchorX = anchor.x === "left" ? start.origin.x : start.origin.x + start.size.width;
401
- const anchorY = anchor.y === "top" ? start.origin.y : start.origin.y + start.size.height;
402
- const maxW = anchor.x === "left" ? bbox.width - anchorX : anchor.x === "right" ? anchorX : Math.min(start.origin.x, bbox.width - start.origin.x - start.size.width) * 2 + start.size.width;
403
- const maxH = anchor.y === "top" ? bbox.height - anchorY : anchor.y === "bottom" ? anchorY : Math.min(start.origin.y, bbox.height - start.origin.y - start.size.height) * 2 + start.size.height;
404
- if (this.config.maintainAspectRatio) {
405
- const scaleW = width > maxW ? maxW / width : 1;
406
- const scaleH = height > maxH ? maxH / height : 1;
407
- const scale = Math.min(scaleW, scaleH);
408
- if (scale < 1) {
409
- width *= scale;
410
- height *= scale;
411
- }
412
- } else {
413
- width = Math.min(width, maxW);
414
- height = Math.min(height, maxH);
415
- }
416
- if (anchor.x === "left") {
417
- x = anchorX;
418
- } else if (anchor.x === "right") {
419
- x = anchorX - width;
420
- } else {
421
- x = start.origin.x + (start.size.width - width) / 2;
422
- }
423
- if (anchor.y === "top") {
424
- y = anchorY;
425
- } else if (anchor.y === "bottom") {
426
- y = anchorY - height;
427
- } else {
428
- y = start.origin.y + (start.size.height - height) / 2;
429
- }
430
- x = Math.max(0, Math.min(x, bbox.width - width));
431
- y = Math.max(0, Math.min(y, bbox.height - height));
432
- return { origin: { x, y }, size: { width, height } };
433
- }
434
- applyConstraints(position) {
435
- const { constraints } = this.config;
436
- if (!constraints) return position;
437
- let {
438
- origin: { x, y },
439
- size: { width, height }
440
- } = position;
441
- const minW = constraints.minWidth ?? 1;
442
- const minH = constraints.minHeight ?? 1;
443
- const maxW = constraints.maxWidth;
444
- const maxH = constraints.maxHeight;
445
- if (this.config.maintainAspectRatio && width > 0 && height > 0) {
446
- const ratio = width / height;
447
- if (width < minW) {
448
- width = minW;
449
- height = width / ratio;
450
- }
451
- if (height < minH) {
452
- height = minH;
453
- width = height * ratio;
454
- }
455
- if (maxW !== void 0 && width > maxW) {
456
- width = maxW;
457
- height = width / ratio;
458
- }
459
- if (maxH !== void 0 && height > maxH) {
460
- height = maxH;
461
- width = height * ratio;
882
+ calculateAngleFromMouse(clientX, clientY) {
883
+ if (!this.centerScreen) return this.initialRotation;
884
+ const dx = clientX - this.centerScreen.x;
885
+ const dy = clientY - this.centerScreen.y;
886
+ const dist = Math.sqrt(dx * dx + dy * dy);
887
+ if (dist < 10) return this.lastComputedRotation;
888
+ const pageRotOffset = (this.config.pageRotation ?? 0) * 90;
889
+ const angleDeg = Math.atan2(dy, dx) * (180 / Math.PI) + 90 - pageRotOffset;
890
+ return normalizeAngle(Math.round(angleDeg));
891
+ }
892
+ applyRotationSnapping(angle) {
893
+ const snapAngles = this.config.rotationSnapAngles ?? [0, 90, 180, 270];
894
+ const threshold = this.config.rotationSnapThreshold ?? 4;
895
+ const normalizedAngle = normalizeAngle(angle);
896
+ for (const candidate of snapAngles) {
897
+ const normalizedCandidate = normalizeAngle(candidate);
898
+ const diff = Math.abs(normalizedAngle - normalizedCandidate);
899
+ const minimalDiff = Math.min(diff, 360 - diff);
900
+ if (minimalDiff <= threshold) {
901
+ return {
902
+ angle: normalizedCandidate,
903
+ isSnapped: true,
904
+ snapTarget: normalizedCandidate
905
+ };
462
906
  }
463
- } else {
464
- width = Math.max(minW, width);
465
- height = Math.max(minH, height);
466
- if (maxW !== void 0) width = Math.min(maxW, width);
467
- if (maxH !== void 0) height = Math.min(maxH, height);
468
- }
469
- if (constraints.boundingBox) {
470
- x = Math.max(0, Math.min(x, constraints.boundingBox.width - width));
471
- y = Math.max(0, Math.min(y, constraints.boundingBox.height - height));
472
907
  }
473
- return { origin: { x, y }, size: { width, height } };
908
+ return { angle: normalizedAngle, isSnapped: false };
474
909
  }
475
910
  }
476
- function diagonalCursor(handle, rot) {
477
- const isOddRotation = rot % 2 === 1;
478
- if (handle === "n" || handle === "s") {
479
- return isOddRotation ? "ew-resize" : "ns-resize";
480
- }
481
- if (handle === "e" || handle === "w") {
482
- return isOddRotation ? "ns-resize" : "ew-resize";
483
- }
484
- const diag0 = {
485
- nw: "nwse-resize",
486
- ne: "nesw-resize",
487
- sw: "nesw-resize",
488
- se: "nwse-resize"
489
- };
490
- if (!isOddRotation) return diag0[handle];
491
- return { nw: "nesw-resize", ne: "nwse-resize", sw: "nwse-resize", se: "nesw-resize" }[handle];
492
- }
493
- function edgeOffset(k, spacing, mode) {
494
- const base = -k / 2;
495
- if (mode === "center") return base;
496
- return mode === "outside" ? base - spacing : base + spacing;
497
- }
498
- function describeResizeFromConfig(cfg, ui = {}) {
499
- const {
500
- handleSize = 8,
501
- spacing = 1,
502
- offsetMode = "outside",
503
- includeSides = false,
504
- zIndex = 3,
505
- rotationAwareCursor = true
506
- } = ui;
507
- const rotation = (cfg.pageRotation ?? 0) % 4;
508
- const off = (edge) => ({
509
- [edge]: edgeOffset(handleSize, spacing, offsetMode) + "px"
510
- });
511
- const corners = [
512
- ["nw", { ...off("top"), ...off("left") }],
513
- ["ne", { ...off("top"), ...off("right") }],
514
- ["sw", { ...off("bottom"), ...off("left") }],
515
- ["se", { ...off("bottom"), ...off("right") }]
516
- ];
517
- const sides = includeSides ? [
518
- ["n", { ...off("top"), left: `calc(50% - ${handleSize / 2}px)` }],
519
- ["s", { ...off("bottom"), left: `calc(50% - ${handleSize / 2}px)` }],
520
- ["w", { ...off("left"), top: `calc(50% - ${handleSize / 2}px)` }],
521
- ["e", { ...off("right"), top: `calc(50% - ${handleSize / 2}px)` }]
522
- ] : [];
523
- const all = [...corners, ...sides];
524
- return all.map(([handle, pos]) => ({
525
- handle,
526
- style: {
527
- position: "absolute",
528
- width: handleSize + "px",
529
- height: handleSize + "px",
530
- borderRadius: "50%",
531
- zIndex,
532
- cursor: rotationAwareCursor ? diagonalCursor(handle, rotation) : "default",
533
- touchAction: "none",
534
- ...pos
535
- },
536
- attrs: { "data-epdf-handle": handle }
537
- }));
538
- }
539
- function describeVerticesFromConfig(cfg, ui = {}, liveVertices) {
540
- const { vertexSize = 12, zIndex = 4 } = ui;
541
- const rect = cfg.element;
542
- const scale = cfg.scale ?? 1;
543
- const verts = liveVertices ?? cfg.vertices ?? [];
544
- return verts.map((v, i) => {
545
- const left = (v.x - rect.origin.x) * scale - vertexSize / 2;
546
- const top = (v.y - rect.origin.y) * scale - vertexSize / 2;
547
- return {
548
- handle: "nw",
549
- // not used; kept for type
550
- style: {
551
- position: "absolute",
552
- left: left + "px",
553
- top: top + "px",
554
- width: vertexSize + "px",
555
- height: vertexSize + "px",
556
- borderRadius: "50%",
557
- cursor: "pointer",
558
- zIndex,
559
- touchAction: "none"
560
- },
561
- attrs: { "data-epdf-vertex": i }
562
- };
563
- });
564
- }
565
911
  function useDragResize(options) {
566
912
  const { onUpdate, enabled = true, ...config } = options;
567
913
  const controllerRef = useRef(null);
568
914
  const onUpdateRef = useRef(onUpdate);
915
+ const activePointerIdRef = useRef(null);
916
+ const activeTargetRef = useRef(null);
569
917
  useEffect(() => {
570
918
  onUpdateRef.current = onUpdate;
571
919
  }, [onUpdate]);
@@ -583,12 +931,81 @@ function useDragResize(options) {
583
931
  }
584
932
  }, [
585
933
  config.element,
934
+ config.rotationCenter,
935
+ config.rotationElement,
586
936
  config.constraints,
587
937
  config.maintainAspectRatio,
588
938
  config.pageRotation,
939
+ config.annotationRotation,
589
940
  config.scale,
590
941
  config.vertices
591
942
  ]);
943
+ const endPointerSession = useCallback((pointerId) => {
944
+ var _a, _b;
945
+ const activePointerId = activePointerIdRef.current;
946
+ const target = activeTargetRef.current;
947
+ const id = pointerId ?? activePointerId;
948
+ if (target && id !== null) {
949
+ try {
950
+ if ((_a = target.hasPointerCapture) == null ? void 0 : _a.call(target, id)) {
951
+ (_b = target.releasePointerCapture) == null ? void 0 : _b.call(target, id);
952
+ }
953
+ } catch {
954
+ }
955
+ }
956
+ activePointerIdRef.current = null;
957
+ activeTargetRef.current = null;
958
+ }, []);
959
+ const startPointerSession = useCallback(
960
+ (e) => {
961
+ var _a;
962
+ if (activePointerIdRef.current !== null && activePointerIdRef.current !== e.pointerId) {
963
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
964
+ endPointerSession(activePointerIdRef.current);
965
+ }
966
+ const target = e.currentTarget;
967
+ activePointerIdRef.current = e.pointerId;
968
+ activeTargetRef.current = target;
969
+ try {
970
+ target.setPointerCapture(e.pointerId);
971
+ } catch {
972
+ }
973
+ },
974
+ [endPointerSession]
975
+ );
976
+ useEffect(() => {
977
+ const eventTarget = globalThis;
978
+ const handleGlobalPointerEnd = (e) => {
979
+ var _a;
980
+ const activePointerId = activePointerIdRef.current;
981
+ if (activePointerId === null || e.pointerId !== activePointerId) return;
982
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
983
+ endPointerSession(e.pointerId);
984
+ };
985
+ const handleWindowBlur = () => {
986
+ var _a;
987
+ if (activePointerIdRef.current === null) return;
988
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
989
+ endPointerSession();
990
+ };
991
+ eventTarget.addEventListener("pointerup", handleGlobalPointerEnd, true);
992
+ eventTarget.addEventListener("pointercancel", handleGlobalPointerEnd, true);
993
+ eventTarget.addEventListener("blur", handleWindowBlur, true);
994
+ return () => {
995
+ eventTarget.removeEventListener("pointerup", handleGlobalPointerEnd, true);
996
+ eventTarget.removeEventListener("pointercancel", handleGlobalPointerEnd, true);
997
+ eventTarget.removeEventListener("blur", handleWindowBlur, true);
998
+ };
999
+ }, [endPointerSession]);
1000
+ useEffect(() => {
1001
+ return () => {
1002
+ var _a;
1003
+ if (activePointerIdRef.current !== null) {
1004
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
1005
+ endPointerSession();
1006
+ }
1007
+ };
1008
+ }, [endPointerSession]);
592
1009
  const handleDragStart = useCallback(
593
1010
  (e) => {
594
1011
  var _a;
@@ -596,23 +1013,46 @@ function useDragResize(options) {
596
1013
  e.preventDefault();
597
1014
  e.stopPropagation();
598
1015
  (_a = controllerRef.current) == null ? void 0 : _a.startDrag(e.clientX, e.clientY);
599
- e.currentTarget.setPointerCapture(e.pointerId);
1016
+ startPointerSession(e);
600
1017
  },
601
- [enabled]
1018
+ [enabled, startPointerSession]
1019
+ );
1020
+ const handleMove = useCallback(
1021
+ (e) => {
1022
+ var _a;
1023
+ e.preventDefault();
1024
+ e.stopPropagation();
1025
+ const activePointerId = activePointerIdRef.current;
1026
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1027
+ (_a = controllerRef.current) == null ? void 0 : _a.move(e.clientX, e.clientY, e.buttons);
1028
+ if (activePointerIdRef.current === e.pointerId && e.buttons === 0) {
1029
+ endPointerSession(e.pointerId);
1030
+ }
1031
+ },
1032
+ [endPointerSession]
1033
+ );
1034
+ const handleEndLike = useCallback(
1035
+ (e) => {
1036
+ var _a;
1037
+ e.preventDefault();
1038
+ e.stopPropagation();
1039
+ const activePointerId = activePointerIdRef.current;
1040
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1041
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
1042
+ endPointerSession(e.pointerId);
1043
+ },
1044
+ [endPointerSession]
1045
+ );
1046
+ const handleLostPointerCapture = useCallback(
1047
+ (e) => {
1048
+ var _a;
1049
+ const activePointerId = activePointerIdRef.current;
1050
+ if (activePointerId === null || e.pointerId !== activePointerId) return;
1051
+ (_a = controllerRef.current) == null ? void 0 : _a.end();
1052
+ endPointerSession(e.pointerId);
1053
+ },
1054
+ [endPointerSession]
602
1055
  );
603
- const handleMove = useCallback((e) => {
604
- var _a;
605
- e.preventDefault();
606
- e.stopPropagation();
607
- (_a = controllerRef.current) == null ? void 0 : _a.move(e.clientX, e.clientY);
608
- }, []);
609
- const handleEnd = useCallback((e) => {
610
- var _a, _b, _c;
611
- e.preventDefault();
612
- e.stopPropagation();
613
- (_a = controllerRef.current) == null ? void 0 : _a.end();
614
- (_c = (_b = e.currentTarget).releasePointerCapture) == null ? void 0 : _c.call(_b, e.pointerId);
615
- }, []);
616
1056
  const createResizeHandler = useCallback(
617
1057
  (handle) => ({
618
1058
  onPointerDown: (e) => {
@@ -621,13 +1061,14 @@ function useDragResize(options) {
621
1061
  e.preventDefault();
622
1062
  e.stopPropagation();
623
1063
  (_a = controllerRef.current) == null ? void 0 : _a.startResize(handle, e.clientX, e.clientY);
624
- e.currentTarget.setPointerCapture(e.pointerId);
1064
+ startPointerSession(e);
625
1065
  },
626
1066
  onPointerMove: handleMove,
627
- onPointerUp: handleEnd,
628
- onPointerCancel: handleEnd
1067
+ onPointerUp: handleEndLike,
1068
+ onPointerCancel: handleEndLike,
1069
+ onLostPointerCapture: handleLostPointerCapture
629
1070
  }),
630
- [enabled, handleMove, handleEnd]
1071
+ [enabled, handleMove, handleEndLike, handleLostPointerCapture, startPointerSession]
631
1072
  );
632
1073
  const createVertexHandler = useCallback(
633
1074
  (vertexIndex) => ({
@@ -637,41 +1078,74 @@ function useDragResize(options) {
637
1078
  e.preventDefault();
638
1079
  e.stopPropagation();
639
1080
  (_a = controllerRef.current) == null ? void 0 : _a.startVertexEdit(vertexIndex, e.clientX, e.clientY);
640
- e.currentTarget.setPointerCapture(e.pointerId);
1081
+ startPointerSession(e);
641
1082
  },
642
1083
  onPointerMove: handleMove,
643
- onPointerUp: handleEnd,
644
- onPointerCancel: handleEnd
1084
+ onPointerUp: handleEndLike,
1085
+ onPointerCancel: handleEndLike,
1086
+ onLostPointerCapture: handleLostPointerCapture
645
1087
  }),
646
- [enabled, handleMove, handleEnd]
1088
+ [enabled, handleMove, handleEndLike, handleLostPointerCapture, startPointerSession]
1089
+ );
1090
+ const createRotationHandler = useCallback(
1091
+ (initialRotation = 0, orbitRadiusPx) => ({
1092
+ onPointerDown: (e) => {
1093
+ var _a;
1094
+ if (!enabled) return;
1095
+ e.preventDefault();
1096
+ e.stopPropagation();
1097
+ const handleRect = e.currentTarget.getBoundingClientRect();
1098
+ const handleCenterX = handleRect.left + handleRect.width / 2;
1099
+ const handleCenterY = handleRect.top + handleRect.height / 2;
1100
+ (_a = controllerRef.current) == null ? void 0 : _a.startRotation(
1101
+ handleCenterX,
1102
+ handleCenterY,
1103
+ initialRotation,
1104
+ orbitRadiusPx
1105
+ );
1106
+ startPointerSession(e);
1107
+ },
1108
+ onPointerMove: handleMove,
1109
+ onPointerUp: handleEndLike,
1110
+ onPointerCancel: handleEndLike,
1111
+ onLostPointerCapture: handleLostPointerCapture
1112
+ }),
1113
+ [enabled, handleMove, handleEndLike, handleLostPointerCapture, startPointerSession]
647
1114
  );
648
1115
  return {
649
1116
  dragProps: enabled ? {
650
1117
  onPointerDown: handleDragStart,
651
1118
  onPointerMove: handleMove,
652
- onPointerUp: handleEnd,
653
- onPointerCancel: handleEnd
1119
+ onPointerUp: handleEndLike,
1120
+ onPointerCancel: handleEndLike,
1121
+ onLostPointerCapture: handleLostPointerCapture
654
1122
  } : {},
655
1123
  createResizeProps: createResizeHandler,
656
- createVertexProps: createVertexHandler
1124
+ createVertexProps: createVertexHandler,
1125
+ createRotationProps: createRotationHandler
657
1126
  };
658
1127
  }
659
1128
  function useInteractionHandles(opts) {
1129
+ var _a, _b, _c, _d, _e, _f;
660
1130
  const {
661
1131
  controller,
662
1132
  resizeUI,
663
1133
  vertexUI,
1134
+ rotationUI,
664
1135
  includeVertices = false,
1136
+ includeRotation = false,
1137
+ currentRotation = 0,
665
1138
  handleAttrs,
666
- vertexAttrs
1139
+ vertexAttrs,
1140
+ rotationAttrs
667
1141
  } = opts;
668
- const { dragProps, createResizeProps, createVertexProps } = useDragResize(controller);
1142
+ const { dragProps, createResizeProps, createVertexProps, createRotationProps } = useDragResize(controller);
669
1143
  const resize = useMemo(() => {
670
1144
  const desc = describeResizeFromConfig(controller, resizeUI);
671
1145
  return desc.map((d) => {
672
- var _a;
1146
+ var _a2;
673
1147
  return {
674
- key: (_a = d.attrs) == null ? void 0 : _a["data-epdf-handle"],
1148
+ key: (_a2 = d.attrs) == null ? void 0 : _a2["data-epdf-handle"],
675
1149
  style: d.style,
676
1150
  ...createResizeProps(d.handle),
677
1151
  ...d.attrs ?? {},
@@ -685,6 +1159,7 @@ function useInteractionHandles(opts) {
685
1159
  controller.element.size.height,
686
1160
  controller.scale,
687
1161
  controller.pageRotation,
1162
+ controller.annotationRotation,
688
1163
  controller.maintainAspectRatio,
689
1164
  resizeUI == null ? void 0 : resizeUI.handleSize,
690
1165
  resizeUI == null ? void 0 : resizeUI.spacing,
@@ -719,7 +1194,44 @@ function useInteractionHandles(opts) {
719
1194
  createVertexProps,
720
1195
  vertexAttrs
721
1196
  ]);
722
- return { dragProps, resize, vertices };
1197
+ const rotation = useMemo(() => {
1198
+ if (!includeRotation) return null;
1199
+ const desc = describeRotationFromConfig(controller, rotationUI, currentRotation);
1200
+ return {
1201
+ handle: {
1202
+ style: desc.handleStyle,
1203
+ ...createRotationProps(currentRotation, desc.radius),
1204
+ ...desc.attrs ?? {},
1205
+ ...(rotationAttrs == null ? void 0 : rotationAttrs()) ?? {}
1206
+ },
1207
+ connector: {
1208
+ style: desc.connectorStyle,
1209
+ "data-epdf-rotation-connector": true
1210
+ }
1211
+ };
1212
+ }, [
1213
+ includeRotation,
1214
+ controller.element.origin.x,
1215
+ controller.element.origin.y,
1216
+ controller.element.size.width,
1217
+ controller.element.size.height,
1218
+ (_a = controller.rotationCenter) == null ? void 0 : _a.x,
1219
+ (_b = controller.rotationCenter) == null ? void 0 : _b.y,
1220
+ (_c = controller.rotationElement) == null ? void 0 : _c.origin.x,
1221
+ (_d = controller.rotationElement) == null ? void 0 : _d.origin.y,
1222
+ (_e = controller.rotationElement) == null ? void 0 : _e.size.width,
1223
+ (_f = controller.rotationElement) == null ? void 0 : _f.size.height,
1224
+ controller.scale,
1225
+ currentRotation,
1226
+ rotationUI == null ? void 0 : rotationUI.handleSize,
1227
+ rotationUI == null ? void 0 : rotationUI.margin,
1228
+ rotationUI == null ? void 0 : rotationUI.zIndex,
1229
+ rotationUI == null ? void 0 : rotationUI.showConnector,
1230
+ rotationUI == null ? void 0 : rotationUI.connectorWidth,
1231
+ createRotationProps,
1232
+ rotationAttrs
1233
+ ]);
1234
+ return { dragProps, resize, vertices, rotation };
723
1235
  }
724
1236
  function useDoublePressProps(onDouble, { delay = 300, tolerancePx = 18 } = {}) {
725
1237
  const last = useRef({ t: 0, x: 0, y: 0 });