@embedpdf/utils 2.4.1 → 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 -317
  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 -317
  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 -288
  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 -292
  46. package/dist/vue/index.js.map +1 -1
  47. package/package.json +2 -2
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,253 +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 diag0 = {
472
- nw: "nwse-resize",
473
- ne: "nesw-resize",
474
- sw: "nesw-resize",
475
- se: "nwse-resize"
476
- };
477
- if (handle === "n" || handle === "s") return "ns-resize";
478
- if (handle === "e" || handle === "w") return "ew-resize";
479
- if (rot % 2 === 0) return diag0[handle];
480
- return { nw: "nesw-resize", ne: "nwse-resize", sw: "nwse-resize", se: "nesw-resize" }[handle];
481
- }
482
- function edgeOffset(k, spacing, mode) {
483
- const base = -k / 2;
484
- if (mode === "center") return base;
485
- return mode === "outside" ? base - spacing : base + spacing;
486
- }
487
- function describeResizeFromConfig(cfg, ui = {}) {
488
- const {
489
- handleSize = 8,
490
- spacing = 1,
491
- offsetMode = "outside",
492
- includeSides = false,
493
- zIndex = 3,
494
- rotationAwareCursor = true
495
- } = ui;
496
- const rotation = (cfg.pageRotation ?? 0) % 4;
497
- const off = (edge) => ({
498
- [edge]: edgeOffset(handleSize, spacing, offsetMode) + "px"
499
- });
500
- const corners = [
501
- ["nw", { ...off("top"), ...off("left") }],
502
- ["ne", { ...off("top"), ...off("right") }],
503
- ["sw", { ...off("bottom"), ...off("left") }],
504
- ["se", { ...off("bottom"), ...off("right") }]
505
- ];
506
- const sides = includeSides ? [
507
- ["n", { ...off("top"), left: `calc(50% - ${handleSize / 2}px)` }],
508
- ["s", { ...off("bottom"), left: `calc(50% - ${handleSize / 2}px)` }],
509
- ["w", { ...off("left"), top: `calc(50% - ${handleSize / 2}px)` }],
510
- ["e", { ...off("right"), top: `calc(50% - ${handleSize / 2}px)` }]
511
- ] : [];
512
- const all = [...corners, ...sides];
513
- return all.map(([handle, pos]) => ({
514
- handle,
515
- style: {
516
- position: "absolute",
517
- width: handleSize + "px",
518
- height: handleSize + "px",
519
- borderRadius: "50%",
520
- zIndex,
521
- cursor: rotationAwareCursor ? diagonalCursor(handle, rotation) : "default",
522
- touchAction: "none",
523
- ...pos
524
- },
525
- attrs: { "data-epdf-handle": handle }
526
- }));
527
- }
528
- function describeVerticesFromConfig(cfg, ui = {}, liveVertices) {
529
- const { vertexSize = 12, zIndex = 4 } = ui;
530
- const rect = cfg.element;
531
- const scale = cfg.scale ?? 1;
532
- const verts = liveVertices ?? cfg.vertices ?? [];
533
- return verts.map((v, i) => {
534
- const left = (v.x - rect.origin.x) * scale - vertexSize / 2;
535
- const top = (v.y - rect.origin.y) * scale - vertexSize / 2;
536
- return {
537
- handle: "nw",
538
- // not used; kept for type
539
- style: {
540
- position: "absolute",
541
- left: left + "px",
542
- top: top + "px",
543
- width: vertexSize + "px",
544
- height: vertexSize + "px",
545
- borderRadius: "50%",
546
- cursor: "pointer",
547
- zIndex,
548
- touchAction: "none"
549
- },
550
- attrs: { "data-epdf-vertex": i }
551
- };
552
- });
553
- }
554
905
  const norm = (v) => toRaw(isRef(v) ? unref(v) : v);
555
906
  const toNum = (n, fallback = 0) => {
556
907
  const v = Number(n);
@@ -572,19 +923,27 @@ function useDragResize(options) {
572
923
  const {
573
924
  onUpdate,
574
925
  element,
926
+ rotationCenter,
927
+ rotationElement,
575
928
  vertices,
576
929
  constraints,
577
930
  maintainAspectRatio,
578
931
  pageRotation,
932
+ annotationRotation,
579
933
  scale,
580
934
  enabled
581
935
  } = options;
582
936
  const initialCfg = {
583
937
  element: rectDTO(norm(element)),
938
+ rotationCenter: rotationCenter ? norm(rotationCenter) : void 0,
939
+ rotationElement: rotationElement ? rectDTO(norm(rotationElement)) : void 0,
584
940
  vertices: vertices ? vertsDTO(norm(vertices)) : void 0,
585
941
  constraints: constraintsDTO(constraints),
586
942
  maintainAspectRatio: boolDTO(enabled === void 0 ? void 0 : norm(maintainAspectRatio)),
587
943
  pageRotation: numDTO(pageRotation === void 0 ? void 0 : norm(pageRotation)),
944
+ annotationRotation: numDTO(
945
+ annotationRotation === void 0 ? void 0 : norm(annotationRotation)
946
+ ),
588
947
  scale: numDTO(scale === void 0 ? void 0 : norm(scale))
589
948
  };
590
949
  if (!controller.value) {
@@ -593,22 +952,30 @@ function useDragResize(options) {
593
952
  watch(
594
953
  () => ({
595
954
  element,
955
+ rotationCenter,
956
+ rotationElement,
596
957
  vertices,
597
958
  constraints,
598
959
  maintainAspectRatio,
599
960
  pageRotation,
961
+ annotationRotation,
600
962
  scale
601
963
  }),
602
964
  (nc) => {
603
965
  var _a;
604
966
  (_a = controller.value) == null ? void 0 : _a.updateConfig({
605
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,
606
970
  vertices: nc.vertices ? vertsDTO(norm(nc.vertices)) : void 0,
607
971
  constraints: constraintsDTO(nc.constraints),
608
972
  maintainAspectRatio: boolDTO(
609
973
  nc.maintainAspectRatio === void 0 ? void 0 : norm(nc.maintainAspectRatio)
610
974
  ),
611
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
+ ),
612
979
  scale: numDTO(nc.scale === void 0 ? void 0 : norm(nc.scale))
613
980
  });
614
981
  },
@@ -628,7 +995,7 @@ function useDragResize(options) {
628
995
  };
629
996
  const handleMove = (e) => {
630
997
  var _a;
631
- 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);
632
999
  };
633
1000
  const handleEnd = (e) => {
634
1001
  var _a, _b, _c;
@@ -666,6 +1033,22 @@ function useDragResize(options) {
666
1033
  onPointerup: handleEnd,
667
1034
  onPointercancel: handleCancel
668
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
+ });
669
1052
  const dragProps = computed(
670
1053
  () => isEnabled() ? {
671
1054
  onPointerdown: handleDragStart,
@@ -674,7 +1057,7 @@ function useDragResize(options) {
674
1057
  onPointercancel: handleCancel
675
1058
  } : {}
676
1059
  );
677
- return { dragProps, createResizeProps, createVertexProps };
1060
+ return { dragProps, createResizeProps, createVertexProps, createRotationProps };
678
1061
  }
679
1062
  function useDoublePressProps(onDouble, { delay = 300, tolerancePx = 18 } = {}) {
680
1063
  const last = ref({ t: 0, x: 0, y: 0 });
@@ -707,27 +1090,41 @@ function useInteractionHandles(opts) {
707
1090
  controller,
708
1091
  resizeUI,
709
1092
  vertexUI,
1093
+ rotationUI,
710
1094
  includeVertices = false,
1095
+ includeRotation = false,
1096
+ currentRotation = 0,
711
1097
  handleAttrs,
712
- vertexAttrs
1098
+ vertexAttrs,
1099
+ rotationAttrs
713
1100
  } = opts;
714
- const { dragProps, createResizeProps, createVertexProps } = useDragResize(controller);
1101
+ const { dragProps, createResizeProps, createVertexProps, createRotationProps } = useDragResize(controller);
715
1102
  const elementPlain = computed(() => rectDTO(norm(controller.element)));
716
1103
  const verticesPlain = computed(
717
1104
  () => controller.vertices ? vertsDTO(norm(controller.vertices)) : void 0
718
1105
  );
719
1106
  const scalePlain = computed(() => Number(norm(controller.scale ?? 1)));
720
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
+ );
721
1111
  const maintainPlain = computed(
722
1112
  () => controller.maintainAspectRatio === void 0 ? void 0 : Boolean(norm(controller.maintainAspectRatio))
723
1113
  );
724
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
+ );
725
1121
  const resize = computed(() => {
726
1122
  const desc = describeResizeFromConfig(
727
1123
  {
728
1124
  element: elementPlain.value,
729
1125
  scale: scalePlain.value,
730
1126
  pageRotation: rotationPlain.value,
1127
+ annotationRotation: annotationRotationPlain.value,
731
1128
  maintainAspectRatio: maintainPlain.value,
732
1129
  constraints: constraintsPlain.value
733
1130
  },
@@ -760,7 +1157,34 @@ function useInteractionHandles(opts) {
760
1157
  ...(vertexAttrs == null ? void 0 : vertexAttrs(i)) ?? {}
761
1158
  }));
762
1159
  });
763
- 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 };
764
1188
  }
765
1189
  function deepToRaw(sourceObj) {
766
1190
  const objectIterator = (input) => {