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