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