@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
@@ -1,12 +1,406 @@
1
1
  import * as $ from "svelte/internal/client";
2
+ import { rotatePointAround, calculateRotatedRectAABB, normalizeAngle } from "@embedpdf/models";
2
3
  import "svelte/internal/disclose-version";
3
4
  import { getCounterRotation } from "@embedpdf/utils";
5
+ const ROTATION_HANDLE_MARGIN = 35;
6
+ const HANDLE_BASE_ANGLE = {
7
+ n: 0,
8
+ ne: 45,
9
+ e: 90,
10
+ se: 135,
11
+ s: 180,
12
+ sw: 225,
13
+ w: 270,
14
+ nw: 315
15
+ };
16
+ const SECTOR_CURSORS = [
17
+ "ns-resize",
18
+ // 0: north
19
+ "nesw-resize",
20
+ // 1: NE
21
+ "ew-resize",
22
+ // 2: east
23
+ "nwse-resize",
24
+ // 3: SE
25
+ "ns-resize",
26
+ // 4: south
27
+ "nesw-resize",
28
+ // 5: SW
29
+ "ew-resize",
30
+ // 6: west
31
+ "nwse-resize"
32
+ // 7: NW
33
+ ];
34
+ function diagonalCursor(handle, pageQuarterTurns, annotationRotation = 0) {
35
+ const pageAngle = pageQuarterTurns * 90;
36
+ const totalAngle = HANDLE_BASE_ANGLE[handle] + pageAngle + annotationRotation;
37
+ const normalized = (totalAngle % 360 + 360) % 360;
38
+ const sector = Math.round(normalized / 45) % 8;
39
+ return SECTOR_CURSORS[sector];
40
+ }
41
+ function edgeOffset(k, spacing, mode) {
42
+ const base = -k / 2;
43
+ if (mode === "center") return base;
44
+ return mode === "outside" ? base - spacing : base + spacing;
45
+ }
46
+ function describeResizeFromConfig(cfg, ui = {}) {
47
+ const {
48
+ handleSize = 8,
49
+ spacing = 1,
50
+ offsetMode = "outside",
51
+ includeSides = false,
52
+ zIndex = 3,
53
+ rotationAwareCursor = true
54
+ } = ui;
55
+ const pageQuarterTurns = (cfg.pageRotation ?? 0) % 4;
56
+ const annotationRot = cfg.annotationRotation ?? 0;
57
+ const off = (edge) => ({
58
+ [edge]: edgeOffset(handleSize, spacing, offsetMode) + "px"
59
+ });
60
+ const corners = [
61
+ ["nw", { ...off("top"), ...off("left") }],
62
+ ["ne", { ...off("top"), ...off("right") }],
63
+ ["sw", { ...off("bottom"), ...off("left") }],
64
+ ["se", { ...off("bottom"), ...off("right") }]
65
+ ];
66
+ const sides = includeSides ? [
67
+ ["n", { ...off("top"), left: `calc(50% - ${handleSize / 2}px)` }],
68
+ ["s", { ...off("bottom"), left: `calc(50% - ${handleSize / 2}px)` }],
69
+ ["w", { ...off("left"), top: `calc(50% - ${handleSize / 2}px)` }],
70
+ ["e", { ...off("right"), top: `calc(50% - ${handleSize / 2}px)` }]
71
+ ] : [];
72
+ const all = [...corners, ...sides];
73
+ return all.map(([handle, pos]) => ({
74
+ handle,
75
+ style: {
76
+ position: "absolute",
77
+ width: handleSize + "px",
78
+ height: handleSize + "px",
79
+ borderRadius: "50%",
80
+ zIndex,
81
+ cursor: rotationAwareCursor ? diagonalCursor(handle, pageQuarterTurns, annotationRot) : "default",
82
+ pointerEvents: "auto",
83
+ touchAction: "none",
84
+ ...pos
85
+ },
86
+ attrs: { "data-epdf-handle": handle }
87
+ }));
88
+ }
89
+ function describeVerticesFromConfig(cfg, ui = {}, liveVertices) {
90
+ const { vertexSize = 12, zIndex = 4 } = ui;
91
+ const rect = cfg.element;
92
+ const scale = cfg.scale ?? 1;
93
+ const verts = liveVertices ?? cfg.vertices ?? [];
94
+ return verts.map((v, i) => {
95
+ const left = (v.x - rect.origin.x) * scale - vertexSize / 2;
96
+ const top = (v.y - rect.origin.y) * scale - vertexSize / 2;
97
+ return {
98
+ handle: "nw",
99
+ // not used; kept for type
100
+ style: {
101
+ position: "absolute",
102
+ left: left + "px",
103
+ top: top + "px",
104
+ width: vertexSize + "px",
105
+ height: vertexSize + "px",
106
+ borderRadius: "50%",
107
+ cursor: "pointer",
108
+ zIndex,
109
+ pointerEvents: "auto",
110
+ touchAction: "none"
111
+ },
112
+ attrs: { "data-epdf-vertex": i }
113
+ };
114
+ });
115
+ }
116
+ function describeRotationFromConfig(cfg, ui = {}, currentAngle = 0) {
117
+ const { handleSize = 16, zIndex = 5, showConnector = true, connectorWidth = 1 } = ui;
118
+ const scale = cfg.scale ?? 1;
119
+ const rect = cfg.element;
120
+ const orbitRect = cfg.rotationElement ?? rect;
121
+ const orbitCenter = cfg.rotationCenter ?? {
122
+ x: rect.origin.x + rect.size.width / 2,
123
+ y: rect.origin.y + rect.size.height / 2
124
+ };
125
+ orbitRect.size.width * scale;
126
+ orbitRect.size.height * scale;
127
+ const centerX = (orbitCenter.x - orbitRect.origin.x) * scale;
128
+ const centerY = (orbitCenter.y - orbitRect.origin.y) * scale;
129
+ const angleRad = currentAngle * Math.PI / 180;
130
+ const margin = ui.margin ?? ROTATION_HANDLE_MARGIN;
131
+ const radius = rect.size.height * scale / 2 + margin;
132
+ const handleCenterX = centerX + radius * Math.sin(angleRad);
133
+ const handleCenterY = centerY - radius * Math.cos(angleRad);
134
+ const handleLeft = handleCenterX - handleSize / 2;
135
+ const handleTop = handleCenterY - handleSize / 2;
136
+ return {
137
+ handleStyle: {
138
+ position: "absolute",
139
+ left: handleLeft + "px",
140
+ top: handleTop + "px",
141
+ width: handleSize + "px",
142
+ height: handleSize + "px",
143
+ borderRadius: "50%",
144
+ cursor: "grab",
145
+ zIndex,
146
+ pointerEvents: "auto",
147
+ touchAction: "none"
148
+ },
149
+ connectorStyle: showConnector ? {
150
+ position: "absolute",
151
+ left: centerX - connectorWidth / 2 + "px",
152
+ top: centerY - radius + "px",
153
+ width: connectorWidth + "px",
154
+ height: radius + "px",
155
+ transformOrigin: "center bottom",
156
+ transform: `rotate(${currentAngle}deg)`,
157
+ zIndex: zIndex - 1,
158
+ pointerEvents: "none"
159
+ } : {},
160
+ radius,
161
+ attrs: { "data-epdf-rotation-handle": true }
162
+ };
163
+ }
4
164
  function getAnchor(handle) {
5
165
  return {
6
166
  x: handle.includes("e") ? "left" : handle.includes("w") ? "right" : "center",
7
167
  y: handle.includes("s") ? "top" : handle.includes("n") ? "bottom" : "center"
8
168
  };
9
169
  }
170
+ function getAnchorPoint(rect, anchor) {
171
+ const x = anchor.x === "left" ? rect.origin.x : anchor.x === "right" ? rect.origin.x + rect.size.width : rect.origin.x + rect.size.width / 2;
172
+ const y = anchor.y === "top" ? rect.origin.y : anchor.y === "bottom" ? rect.origin.y + rect.size.height : rect.origin.y + rect.size.height / 2;
173
+ return { x, y };
174
+ }
175
+ function applyResizeDelta(startRect, delta, anchor) {
176
+ let x = startRect.origin.x;
177
+ let y = startRect.origin.y;
178
+ let width = startRect.size.width;
179
+ let height = startRect.size.height;
180
+ if (anchor.x === "left") {
181
+ width += delta.x;
182
+ } else if (anchor.x === "right") {
183
+ x += delta.x;
184
+ width -= delta.x;
185
+ }
186
+ if (anchor.y === "top") {
187
+ height += delta.y;
188
+ } else if (anchor.y === "bottom") {
189
+ y += delta.y;
190
+ height -= delta.y;
191
+ }
192
+ return { origin: { x, y }, size: { width, height } };
193
+ }
194
+ function enforceAspectRatio(rect, startRect, anchor, aspectRatio) {
195
+ let { x, y } = rect.origin;
196
+ let { width, height } = rect.size;
197
+ const isEdgeHandle = anchor.x === "center" || anchor.y === "center";
198
+ if (isEdgeHandle) {
199
+ if (anchor.y === "center") {
200
+ height = width / aspectRatio;
201
+ y = startRect.origin.y + (startRect.size.height - height) / 2;
202
+ } else {
203
+ width = height * aspectRatio;
204
+ x = startRect.origin.x + (startRect.size.width - width) / 2;
205
+ }
206
+ } else {
207
+ const dw = Math.abs(width - startRect.size.width);
208
+ const dh = Math.abs(height - startRect.size.height);
209
+ const total = dw + dh;
210
+ if (total === 0) {
211
+ width = startRect.size.width;
212
+ height = startRect.size.height;
213
+ } else {
214
+ const wWeight = dw / total;
215
+ const hWeight = dh / total;
216
+ const wFromW = width;
217
+ const hFromW = width / aspectRatio;
218
+ const wFromH = height * aspectRatio;
219
+ const hFromH = height;
220
+ width = wWeight * wFromW + hWeight * wFromH;
221
+ height = wWeight * hFromW + hWeight * hFromH;
222
+ }
223
+ }
224
+ if (anchor.x === "right") {
225
+ x = startRect.origin.x + startRect.size.width - width;
226
+ }
227
+ if (anchor.y === "bottom") {
228
+ y = startRect.origin.y + startRect.size.height - height;
229
+ }
230
+ return { origin: { x, y }, size: { width, height } };
231
+ }
232
+ function clampToBounds(rect, startRect, anchor, bbox, maintainAspectRatio) {
233
+ if (!bbox) return rect;
234
+ let { x, y } = rect.origin;
235
+ let { width, height } = rect.size;
236
+ width = Math.max(1, width);
237
+ height = Math.max(1, height);
238
+ const anchorX = anchor.x === "left" ? startRect.origin.x : startRect.origin.x + startRect.size.width;
239
+ const anchorY = anchor.y === "top" ? startRect.origin.y : startRect.origin.y + startRect.size.height;
240
+ 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;
241
+ 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;
242
+ if (maintainAspectRatio) {
243
+ const scaleW = width > maxW ? maxW / width : 1;
244
+ const scaleH = height > maxH ? maxH / height : 1;
245
+ const scale = Math.min(scaleW, scaleH);
246
+ if (scale < 1) {
247
+ width *= scale;
248
+ height *= scale;
249
+ }
250
+ } else {
251
+ width = Math.min(width, maxW);
252
+ height = Math.min(height, maxH);
253
+ }
254
+ if (anchor.x === "left") {
255
+ x = anchorX;
256
+ } else if (anchor.x === "right") {
257
+ x = anchorX - width;
258
+ } else {
259
+ x = startRect.origin.x + (startRect.size.width - width) / 2;
260
+ }
261
+ if (anchor.y === "top") {
262
+ y = anchorY;
263
+ } else if (anchor.y === "bottom") {
264
+ y = anchorY - height;
265
+ } else {
266
+ y = startRect.origin.y + (startRect.size.height - height) / 2;
267
+ }
268
+ x = Math.max(0, Math.min(x, bbox.width - width));
269
+ y = Math.max(0, Math.min(y, bbox.height - height));
270
+ return { origin: { x, y }, size: { width, height } };
271
+ }
272
+ function reanchorRect(rect, startRect, anchor) {
273
+ let x;
274
+ let y;
275
+ if (anchor.x === "left") {
276
+ x = startRect.origin.x;
277
+ } else if (anchor.x === "right") {
278
+ x = startRect.origin.x + startRect.size.width - rect.size.width;
279
+ } else {
280
+ x = startRect.origin.x + (startRect.size.width - rect.size.width) / 2;
281
+ }
282
+ if (anchor.y === "top") {
283
+ y = startRect.origin.y;
284
+ } else if (anchor.y === "bottom") {
285
+ y = startRect.origin.y + startRect.size.height - rect.size.height;
286
+ } else {
287
+ y = startRect.origin.y + (startRect.size.height - rect.size.height) / 2;
288
+ }
289
+ return { origin: { x, y }, size: rect.size };
290
+ }
291
+ function applyConstraints(position, constraints, maintainAspectRatio, skipBoundingClamp = false) {
292
+ if (!constraints) return position;
293
+ let {
294
+ origin: { x, y },
295
+ size: { width, height }
296
+ } = position;
297
+ const minW = constraints.minWidth ?? 1;
298
+ const minH = constraints.minHeight ?? 1;
299
+ const maxW = constraints.maxWidth;
300
+ const maxH = constraints.maxHeight;
301
+ if (maintainAspectRatio && width > 0 && height > 0) {
302
+ const ratio = width / height;
303
+ if (width < minW) {
304
+ width = minW;
305
+ height = width / ratio;
306
+ }
307
+ if (height < minH) {
308
+ height = minH;
309
+ width = height * ratio;
310
+ }
311
+ if (maxW !== void 0 && width > maxW) {
312
+ width = maxW;
313
+ height = width / ratio;
314
+ }
315
+ if (maxH !== void 0 && height > maxH) {
316
+ height = maxH;
317
+ width = height * ratio;
318
+ }
319
+ } else {
320
+ width = Math.max(minW, width);
321
+ height = Math.max(minH, height);
322
+ if (maxW !== void 0) width = Math.min(maxW, width);
323
+ if (maxH !== void 0) height = Math.min(maxH, height);
324
+ }
325
+ if (constraints.boundingBox && !skipBoundingClamp) {
326
+ x = Math.max(0, Math.min(x, constraints.boundingBox.width - width));
327
+ y = Math.max(0, Math.min(y, constraints.boundingBox.height - height));
328
+ }
329
+ return { origin: { x, y }, size: { width, height } };
330
+ }
331
+ function isRectWithinRotatedBounds(rect, angleDegrees, bbox) {
332
+ const eps = 1e-6;
333
+ const aabb = calculateRotatedRectAABB(rect, angleDegrees);
334
+ 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;
335
+ }
336
+ function computeResizeStep(delta, handle, config, clampLocalBounds, skipConstraintBoundingClamp) {
337
+ const { startRect, maintainAspectRatio = false, annotationRotation = 0, constraints } = config;
338
+ const anchor = getAnchor(handle);
339
+ const aspectRatio = startRect.size.width / startRect.size.height || 1;
340
+ let rect = applyResizeDelta(startRect, delta, anchor);
341
+ if (maintainAspectRatio) {
342
+ rect = enforceAspectRatio(rect, startRect, anchor, aspectRatio);
343
+ }
344
+ if (clampLocalBounds) {
345
+ rect = clampToBounds(rect, startRect, anchor, constraints == null ? void 0 : constraints.boundingBox, maintainAspectRatio);
346
+ }
347
+ rect = applyConstraints(rect, constraints, maintainAspectRatio, skipConstraintBoundingClamp);
348
+ if (skipConstraintBoundingClamp) {
349
+ rect = reanchorRect(rect, startRect, anchor);
350
+ }
351
+ if (annotationRotation !== 0) {
352
+ const anchorPt = getAnchorPoint(startRect, anchor);
353
+ const oldCenter = {
354
+ x: startRect.origin.x + startRect.size.width / 2,
355
+ y: startRect.origin.y + startRect.size.height / 2
356
+ };
357
+ const newCenter = {
358
+ x: rect.origin.x + rect.size.width / 2,
359
+ y: rect.origin.y + rect.size.height / 2
360
+ };
361
+ const oldVisual = rotatePointAround(anchorPt, oldCenter, annotationRotation);
362
+ const newVisual = rotatePointAround(anchorPt, newCenter, annotationRotation);
363
+ rect = {
364
+ origin: {
365
+ x: rect.origin.x + (oldVisual.x - newVisual.x),
366
+ y: rect.origin.y + (oldVisual.y - newVisual.y)
367
+ },
368
+ size: rect.size
369
+ };
370
+ }
371
+ return rect;
372
+ }
373
+ function computeResizedRect(delta, handle, config) {
374
+ const { annotationRotation = 0, constraints } = config;
375
+ const bbox = constraints == null ? void 0 : constraints.boundingBox;
376
+ if (annotationRotation !== 0 && bbox) {
377
+ const target = computeResizeStep(delta, handle, config, false, true);
378
+ if (isRectWithinRotatedBounds(target, annotationRotation, bbox)) {
379
+ return target;
380
+ }
381
+ let best = computeResizeStep({ x: 0, y: 0 }, handle, config, false, true);
382
+ let low = 0;
383
+ let high = 1;
384
+ for (let i = 0; i < 20; i += 1) {
385
+ const mid = (low + high) / 2;
386
+ const trial = computeResizeStep(
387
+ { x: delta.x * mid, y: delta.y * mid },
388
+ handle,
389
+ config,
390
+ false,
391
+ true
392
+ );
393
+ if (isRectWithinRotatedBounds(trial, annotationRotation, bbox)) {
394
+ best = trial;
395
+ low = mid;
396
+ } else {
397
+ high = mid;
398
+ }
399
+ }
400
+ return best;
401
+ }
402
+ return computeResizeStep(delta, handle, config, true, false);
403
+ }
10
404
  class DragResizeController {
11
405
  constructor(config, onUpdate) {
12
406
  this.config = config;
@@ -14,29 +408,41 @@ class DragResizeController {
14
408
  this.state = "idle";
15
409
  this.startPoint = null;
16
410
  this.startElement = null;
411
+ this.startRotationElement = null;
412
+ this.gestureRotationCenter = null;
17
413
  this.activeHandle = null;
18
414
  this.currentPosition = null;
19
415
  this.activeVertexIndex = null;
20
416
  this.startVertices = [];
21
417
  this.currentVertices = [];
418
+ this.rotationCenter = null;
419
+ this.centerScreen = null;
420
+ this.initialRotation = 0;
421
+ this.lastComputedRotation = 0;
422
+ this.rotationDelta = 0;
423
+ this.rotationSnappedAngle = null;
22
424
  this.currentVertices = config.vertices || [];
23
425
  }
24
426
  updateConfig(config) {
25
427
  this.config = { ...this.config, ...config };
26
- this.currentVertices = config.vertices || [];
428
+ if (this.state !== "vertex-editing") {
429
+ this.currentVertices = config.vertices || [];
430
+ }
27
431
  }
432
+ // ---------------------------------------------------------------------------
433
+ // Gesture start
434
+ // ---------------------------------------------------------------------------
28
435
  startDrag(clientX, clientY) {
29
436
  this.state = "dragging";
30
437
  this.startPoint = { x: clientX, y: clientY };
31
438
  this.startElement = { ...this.config.element };
439
+ this.startRotationElement = this.config.rotationElement ? { ...this.config.rotationElement } : null;
32
440
  this.currentPosition = { ...this.config.element };
33
441
  this.onUpdate({
34
442
  state: "start",
35
443
  transformData: {
36
444
  type: "move",
37
- changes: {
38
- rect: this.startElement
39
- }
445
+ changes: { rect: this.startElement }
40
446
  }
41
447
  });
42
448
  }
@@ -50,9 +456,7 @@ class DragResizeController {
50
456
  state: "start",
51
457
  transformData: {
52
458
  type: "resize",
53
- changes: {
54
- rect: this.startElement
55
- },
459
+ changes: { rect: this.startElement },
56
460
  metadata: {
57
461
  handle: this.activeHandle,
58
462
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -67,45 +471,87 @@ class DragResizeController {
67
471
  this.activeVertexIndex = vertexIndex;
68
472
  this.startPoint = { x: clientX, y: clientY };
69
473
  this.startVertices = [...this.currentVertices];
474
+ this.gestureRotationCenter = this.config.rotationCenter ?? {
475
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
476
+ y: this.config.element.origin.y + this.config.element.size.height / 2
477
+ };
70
478
  this.onUpdate({
71
479
  state: "start",
72
480
  transformData: {
73
481
  type: "vertex-edit",
74
- changes: {
75
- vertices: this.startVertices
76
- },
482
+ changes: { vertices: this.startVertices },
483
+ metadata: { vertexIndex }
484
+ }
485
+ });
486
+ }
487
+ startRotation(clientX, clientY, initialRotation = 0, orbitRadiusPx) {
488
+ this.state = "rotating";
489
+ this.startPoint = { x: clientX, y: clientY };
490
+ this.startElement = { ...this.config.element };
491
+ this.rotationCenter = this.config.rotationCenter ?? {
492
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
493
+ y: this.config.element.origin.y + this.config.element.size.height / 2
494
+ };
495
+ const { scale = 1 } = this.config;
496
+ const orbitRect = this.config.rotationElement ?? this.config.element;
497
+ const sw = orbitRect.size.width * scale;
498
+ const sh = orbitRect.size.height * scale;
499
+ const radius = orbitRadiusPx ?? Math.max(sw, sh) / 2 + ROTATION_HANDLE_MARGIN;
500
+ const pageRotOffset = (this.config.pageRotation ?? 0) * 90;
501
+ const screenAngleRad = (initialRotation + pageRotOffset) * Math.PI / 180;
502
+ this.centerScreen = {
503
+ x: clientX - radius * Math.sin(screenAngleRad),
504
+ y: clientY + radius * Math.cos(screenAngleRad)
505
+ };
506
+ this.initialRotation = initialRotation;
507
+ this.lastComputedRotation = initialRotation;
508
+ this.rotationDelta = 0;
509
+ this.rotationSnappedAngle = null;
510
+ this.onUpdate({
511
+ state: "start",
512
+ transformData: {
513
+ type: "rotate",
514
+ changes: { rotation: initialRotation },
77
515
  metadata: {
78
- vertexIndex
516
+ rotationAngle: initialRotation,
517
+ rotationDelta: 0,
518
+ rotationCenter: this.rotationCenter,
519
+ isSnapped: false
79
520
  }
80
521
  }
81
522
  });
82
523
  }
83
- move(clientX, clientY) {
524
+ // ---------------------------------------------------------------------------
525
+ // Gesture move
526
+ // ---------------------------------------------------------------------------
527
+ move(clientX, clientY, buttons) {
84
528
  if (this.state === "idle" || !this.startPoint) return;
529
+ if (buttons !== void 0 && buttons === 0) {
530
+ this.end();
531
+ return;
532
+ }
85
533
  if (this.state === "dragging" && this.startElement) {
86
534
  const delta = this.calculateDelta(clientX, clientY);
87
535
  const position = this.calculateDragPosition(delta);
88
536
  this.currentPosition = position;
89
537
  this.onUpdate({
90
538
  state: "move",
91
- transformData: {
92
- type: "move",
93
- changes: {
94
- rect: position
95
- }
96
- }
539
+ transformData: { type: "move", changes: { rect: position } }
97
540
  });
98
541
  } else if (this.state === "resizing" && this.activeHandle && this.startElement) {
99
- const delta = this.calculateDelta(clientX, clientY);
100
- const position = this.calculateResizePosition(delta, this.activeHandle);
542
+ const delta = this.calculateLocalDelta(clientX, clientY);
543
+ const position = computeResizedRect(delta, this.activeHandle, {
544
+ startRect: this.startElement,
545
+ maintainAspectRatio: this.config.maintainAspectRatio,
546
+ annotationRotation: this.config.annotationRotation,
547
+ constraints: this.config.constraints
548
+ });
101
549
  this.currentPosition = position;
102
550
  this.onUpdate({
103
551
  state: "move",
104
552
  transformData: {
105
553
  type: "resize",
106
- changes: {
107
- rect: position
108
- },
554
+ changes: { rect: position },
109
555
  metadata: {
110
556
  handle: this.activeHandle,
111
557
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -119,16 +565,40 @@ class DragResizeController {
119
565
  state: "move",
120
566
  transformData: {
121
567
  type: "vertex-edit",
122
- changes: {
123
- vertices
124
- },
568
+ changes: { vertices },
569
+ metadata: { vertexIndex: this.activeVertexIndex }
570
+ }
571
+ });
572
+ } else if (this.state === "rotating" && this.rotationCenter) {
573
+ const absoluteAngle = this.calculateAngleFromMouse(clientX, clientY);
574
+ const snapResult = this.applyRotationSnapping(absoluteAngle);
575
+ const snappedAngle = normalizeAngle(snapResult.angle);
576
+ const previousAngle = this.lastComputedRotation;
577
+ const rawDelta = snappedAngle - previousAngle;
578
+ const adjustedDelta = rawDelta > 180 ? rawDelta - 360 : rawDelta < -180 ? rawDelta + 360 : rawDelta;
579
+ this.rotationDelta += adjustedDelta;
580
+ this.lastComputedRotation = snappedAngle;
581
+ this.rotationSnappedAngle = snapResult.isSnapped ? snappedAngle : null;
582
+ this.onUpdate({
583
+ state: "move",
584
+ transformData: {
585
+ type: "rotate",
586
+ changes: { rotation: snappedAngle },
125
587
  metadata: {
126
- vertexIndex: this.activeVertexIndex
588
+ rotationAngle: snappedAngle,
589
+ rotationDelta: this.rotationDelta,
590
+ rotationCenter: this.rotationCenter,
591
+ isSnapped: snapResult.isSnapped,
592
+ snappedAngle: this.rotationSnappedAngle ?? void 0,
593
+ cursorPosition: { clientX, clientY }
127
594
  }
128
595
  }
129
596
  });
130
597
  }
131
598
  }
599
+ // ---------------------------------------------------------------------------
600
+ // Gesture end / cancel
601
+ // ---------------------------------------------------------------------------
132
602
  end() {
133
603
  if (this.state === "idle") return;
134
604
  const wasState = this.state;
@@ -139,23 +609,32 @@ class DragResizeController {
139
609
  state: "end",
140
610
  transformData: {
141
611
  type: "vertex-edit",
142
- changes: {
143
- vertices: this.currentVertices
144
- },
612
+ changes: { vertices: this.currentVertices },
613
+ metadata: { vertexIndex: vertexIndex || void 0 }
614
+ }
615
+ });
616
+ } else if (wasState === "rotating") {
617
+ this.onUpdate({
618
+ state: "end",
619
+ transformData: {
620
+ type: "rotate",
621
+ changes: { rotation: this.lastComputedRotation },
145
622
  metadata: {
146
- vertexIndex: vertexIndex || void 0
623
+ rotationAngle: this.lastComputedRotation,
624
+ rotationDelta: this.rotationDelta,
625
+ rotationCenter: this.rotationCenter || void 0,
626
+ isSnapped: this.rotationSnappedAngle !== null,
627
+ snappedAngle: this.rotationSnappedAngle ?? void 0
147
628
  }
148
629
  }
149
630
  });
150
631
  } else {
151
- const finalPosition = this.getCurrentPosition();
632
+ const finalPosition = this.currentPosition || this.config.element;
152
633
  this.onUpdate({
153
634
  state: "end",
154
635
  transformData: {
155
636
  type: wasState === "dragging" ? "move" : "resize",
156
- changes: {
157
- rect: finalPosition
158
- },
637
+ changes: { rect: finalPosition },
159
638
  metadata: wasState === "dragging" ? void 0 : {
160
639
  handle: handle || void 0,
161
640
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -172,11 +651,21 @@ class DragResizeController {
172
651
  state: "end",
173
652
  transformData: {
174
653
  type: "vertex-edit",
175
- changes: {
176
- vertices: this.startVertices
177
- },
654
+ changes: { vertices: this.startVertices },
655
+ metadata: { vertexIndex: this.activeVertexIndex || void 0 }
656
+ }
657
+ });
658
+ } else if (this.state === "rotating") {
659
+ this.onUpdate({
660
+ state: "end",
661
+ transformData: {
662
+ type: "rotate",
663
+ changes: { rotation: this.initialRotation },
178
664
  metadata: {
179
- vertexIndex: this.activeVertexIndex || void 0
665
+ rotationAngle: this.initialRotation,
666
+ rotationDelta: 0,
667
+ rotationCenter: this.rotationCenter || void 0,
668
+ isSnapped: false
180
669
  }
181
670
  }
182
671
  });
@@ -185,9 +674,7 @@ class DragResizeController {
185
674
  state: "end",
186
675
  transformData: {
187
676
  type: this.state === "dragging" ? "move" : "resize",
188
- changes: {
189
- rect: this.startElement
190
- },
677
+ changes: { rect: this.startElement },
191
678
  metadata: this.state === "dragging" ? void 0 : {
192
679
  handle: this.activeHandle || void 0,
193
680
  maintainAspectRatio: this.config.maintainAspectRatio
@@ -197,18 +684,29 @@ class DragResizeController {
197
684
  }
198
685
  this.reset();
199
686
  }
687
+ // ---------------------------------------------------------------------------
688
+ // Private: state management
689
+ // ---------------------------------------------------------------------------
200
690
  reset() {
201
691
  this.state = "idle";
202
692
  this.startPoint = null;
203
693
  this.startElement = null;
694
+ this.startRotationElement = null;
695
+ this.gestureRotationCenter = null;
204
696
  this.activeHandle = null;
205
697
  this.currentPosition = null;
206
698
  this.activeVertexIndex = null;
207
699
  this.startVertices = [];
700
+ this.rotationCenter = null;
701
+ this.centerScreen = null;
702
+ this.initialRotation = 0;
703
+ this.lastComputedRotation = 0;
704
+ this.rotationDelta = 0;
705
+ this.rotationSnappedAngle = null;
208
706
  }
209
- getCurrentPosition() {
210
- return this.currentPosition || this.config.element;
211
- }
707
+ // ---------------------------------------------------------------------------
708
+ // Private: coordinate transformation (screen → page → local)
709
+ // ---------------------------------------------------------------------------
212
710
  calculateDelta(clientX, clientY) {
213
711
  if (!this.startPoint) return { x: 0, y: 0 };
214
712
  const rawDelta = {
@@ -229,18 +727,50 @@ class DragResizeController {
229
727
  y: -sin * scaledX + cos * scaledY
230
728
  };
231
729
  }
730
+ /**
731
+ * Calculate delta projected into the annotation's local (unrotated) coordinate space.
732
+ * Used for resize and vertex-edit where mouse movement must be mapped to the
733
+ * annotation's own axes, accounting for its rotation.
734
+ */
735
+ calculateLocalDelta(clientX, clientY) {
736
+ const pageDelta = this.calculateDelta(clientX, clientY);
737
+ const { annotationRotation = 0 } = this.config;
738
+ if (annotationRotation === 0) return pageDelta;
739
+ const rad = annotationRotation * Math.PI / 180;
740
+ const cos = Math.cos(rad);
741
+ const sin = Math.sin(rad);
742
+ return {
743
+ x: cos * pageDelta.x + sin * pageDelta.y,
744
+ y: -sin * pageDelta.x + cos * pageDelta.y
745
+ };
746
+ }
747
+ // ---------------------------------------------------------------------------
748
+ // Private: vertex clamping
749
+ // ---------------------------------------------------------------------------
232
750
  clampPoint(p) {
233
751
  var _a;
234
752
  const bbox = (_a = this.config.constraints) == null ? void 0 : _a.boundingBox;
235
753
  if (!bbox) return p;
236
- return {
237
- x: Math.max(0, Math.min(p.x, bbox.width)),
238
- y: Math.max(0, Math.min(p.y, bbox.height))
754
+ const { annotationRotation = 0 } = this.config;
755
+ if (annotationRotation === 0) {
756
+ return {
757
+ x: Math.max(0, Math.min(p.x, bbox.width)),
758
+ y: Math.max(0, Math.min(p.y, bbox.height))
759
+ };
760
+ }
761
+ const center = this.gestureRotationCenter ?? this.config.rotationCenter ?? {
762
+ x: this.config.element.origin.x + this.config.element.size.width / 2,
763
+ y: this.config.element.origin.y + this.config.element.size.height / 2
239
764
  };
765
+ const visual = rotatePointAround(p, center, annotationRotation);
766
+ const clampedX = Math.max(0, Math.min(visual.x, bbox.width));
767
+ const clampedY = Math.max(0, Math.min(visual.y, bbox.height));
768
+ if (clampedX === visual.x && clampedY === visual.y) return p;
769
+ return rotatePointAround({ x: clampedX, y: clampedY }, center, -annotationRotation);
240
770
  }
241
771
  calculateVertexPosition(clientX, clientY) {
242
772
  if (this.activeVertexIndex === null) return this.startVertices;
243
- const delta = this.calculateDelta(clientX, clientY);
773
+ const delta = this.calculateLocalDelta(clientX, clientY);
244
774
  const newVertices = [...this.startVertices];
245
775
  const currentVertex = newVertices[this.activeVertexIndex];
246
776
  const moved = {
@@ -250,6 +780,9 @@ class DragResizeController {
250
780
  newVertices[this.activeVertexIndex] = this.clampPoint(moved);
251
781
  return newVertices;
252
782
  }
783
+ // ---------------------------------------------------------------------------
784
+ // Private: drag position
785
+ // ---------------------------------------------------------------------------
253
786
  calculateDragPosition(delta) {
254
787
  if (!this.startElement) return this.config.element;
255
788
  const position = {
@@ -262,253 +795,71 @@ class DragResizeController {
262
795
  height: this.startElement.size.height
263
796
  }
264
797
  };
265
- return this.applyConstraints(position);
266
- }
267
- /**
268
- * Calculate the new rect after a resize operation.
269
- * Pipeline: applyDelta → enforceAspectRatio → clampToBounds → applyConstraints
270
- */
271
- calculateResizePosition(delta, handle) {
272
- if (!this.startElement) return this.config.element;
273
- const anchor = getAnchor(handle);
274
- const aspectRatio = this.startElement.size.width / this.startElement.size.height || 1;
275
- let rect = this.applyResizeDelta(delta, anchor);
276
- if (this.config.maintainAspectRatio) {
277
- rect = this.enforceAspectRatio(rect, anchor, aspectRatio);
278
- }
279
- rect = this.clampToBounds(rect, anchor, aspectRatio);
280
- return this.applyConstraints(rect);
281
- }
282
- /**
283
- * Apply the mouse delta to produce a raw (unconstrained) resized rect.
284
- */
285
- applyResizeDelta(delta, anchor) {
286
- const start = this.startElement;
287
- let x = start.origin.x;
288
- let y = start.origin.y;
289
- let width = start.size.width;
290
- let height = start.size.height;
291
- if (anchor.x === "left") {
292
- width += delta.x;
293
- } else if (anchor.x === "right") {
294
- x += delta.x;
295
- width -= delta.x;
296
- }
297
- if (anchor.y === "top") {
298
- height += delta.y;
299
- } else if (anchor.y === "bottom") {
300
- y += delta.y;
301
- height -= delta.y;
302
- }
303
- return { origin: { x, y }, size: { width, height } };
304
- }
305
- /**
306
- * Enforce aspect ratio while respecting the anchor.
307
- * For edge handles (center anchor on one axis), the rect expands symmetrically on that axis.
308
- * For corner handles, the anchor corner stays fixed.
309
- */
310
- enforceAspectRatio(rect, anchor, aspectRatio) {
311
- const start = this.startElement;
312
- let { x, y } = rect.origin;
313
- let { width, height } = rect.size;
314
- const isEdgeHandle = anchor.x === "center" || anchor.y === "center";
315
- if (isEdgeHandle) {
316
- if (anchor.y === "center") {
317
- height = width / aspectRatio;
318
- y = start.origin.y + (start.size.height - height) / 2;
798
+ const { annotationRotation = 0, constraints } = this.config;
799
+ const bbox = constraints == null ? void 0 : constraints.boundingBox;
800
+ if (annotationRotation !== 0 && bbox) {
801
+ let aabbW;
802
+ let aabbH;
803
+ let offsetX;
804
+ let offsetY;
805
+ if (this.startRotationElement) {
806
+ aabbW = this.startRotationElement.size.width;
807
+ aabbH = this.startRotationElement.size.height;
808
+ offsetX = this.startRotationElement.origin.x - this.startElement.origin.x;
809
+ offsetY = this.startRotationElement.origin.y - this.startElement.origin.y;
319
810
  } else {
320
- width = height * aspectRatio;
321
- x = start.origin.x + (start.size.width - width) / 2;
811
+ const rad = Math.abs(annotationRotation * Math.PI / 180);
812
+ const cos = Math.abs(Math.cos(rad));
813
+ const sin = Math.abs(Math.sin(rad));
814
+ const w = position.size.width;
815
+ const h = position.size.height;
816
+ aabbW = w * cos + h * sin;
817
+ aabbH = w * sin + h * cos;
818
+ offsetX = (w - aabbW) / 2;
819
+ offsetY = (h - aabbH) / 2;
322
820
  }
323
- } else {
324
- const dw = Math.abs(width - start.size.width);
325
- const dh = Math.abs(height - start.size.height);
326
- if (dw >= dh) {
327
- height = width / aspectRatio;
328
- } else {
329
- width = height * aspectRatio;
330
- }
331
- }
332
- if (anchor.x === "right") {
333
- x = start.origin.x + start.size.width - width;
821
+ let { x, y } = position.origin;
822
+ x = Math.max(-offsetX, Math.min(x, bbox.width - aabbW - offsetX));
823
+ y = Math.max(-offsetY, Math.min(y, bbox.height - aabbH - offsetY));
824
+ return { origin: { x, y }, size: position.size };
334
825
  }
335
- if (anchor.y === "bottom") {
336
- y = start.origin.y + start.size.height - height;
337
- }
338
- return { origin: { x, y }, size: { width, height } };
826
+ return applyConstraints(position, constraints, this.config.maintainAspectRatio ?? false);
339
827
  }
828
+ // ---------------------------------------------------------------------------
829
+ // Private: rotation
830
+ // ---------------------------------------------------------------------------
340
831
  /**
341
- * Clamp rect to bounding box while respecting anchor and aspect ratio.
832
+ * Calculate the angle from the center to a point in screen coordinates.
342
833
  */
343
- clampToBounds(rect, anchor, aspectRatio) {
344
- var _a;
345
- const bbox = (_a = this.config.constraints) == null ? void 0 : _a.boundingBox;
346
- if (!bbox) return rect;
347
- const start = this.startElement;
348
- let { x, y } = rect.origin;
349
- let { width, height } = rect.size;
350
- width = Math.max(1, width);
351
- height = Math.max(1, height);
352
- const anchorX = anchor.x === "left" ? start.origin.x : start.origin.x + start.size.width;
353
- const anchorY = anchor.y === "top" ? start.origin.y : start.origin.y + start.size.height;
354
- 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;
355
- 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;
356
- if (this.config.maintainAspectRatio) {
357
- const scaleW = width > maxW ? maxW / width : 1;
358
- const scaleH = height > maxH ? maxH / height : 1;
359
- const scale = Math.min(scaleW, scaleH);
360
- if (scale < 1) {
361
- width *= scale;
362
- height *= scale;
834
+ calculateAngleFromMouse(clientX, clientY) {
835
+ if (!this.centerScreen) return this.initialRotation;
836
+ const dx = clientX - this.centerScreen.x;
837
+ const dy = clientY - this.centerScreen.y;
838
+ const dist = Math.sqrt(dx * dx + dy * dy);
839
+ if (dist < 10) return this.lastComputedRotation;
840
+ const pageRotOffset = (this.config.pageRotation ?? 0) * 90;
841
+ const angleDeg = Math.atan2(dy, dx) * (180 / Math.PI) + 90 - pageRotOffset;
842
+ return normalizeAngle(Math.round(angleDeg));
843
+ }
844
+ applyRotationSnapping(angle) {
845
+ const snapAngles = this.config.rotationSnapAngles ?? [0, 90, 180, 270];
846
+ const threshold = this.config.rotationSnapThreshold ?? 4;
847
+ const normalizedAngle = normalizeAngle(angle);
848
+ for (const candidate of snapAngles) {
849
+ const normalizedCandidate = normalizeAngle(candidate);
850
+ const diff = Math.abs(normalizedAngle - normalizedCandidate);
851
+ const minimalDiff = Math.min(diff, 360 - diff);
852
+ if (minimalDiff <= threshold) {
853
+ return {
854
+ angle: normalizedCandidate,
855
+ isSnapped: true,
856
+ snapTarget: normalizedCandidate
857
+ };
363
858
  }
364
- } else {
365
- width = Math.min(width, maxW);
366
- height = Math.min(height, maxH);
367
859
  }
368
- if (anchor.x === "left") {
369
- x = anchorX;
370
- } else if (anchor.x === "right") {
371
- x = anchorX - width;
372
- } else {
373
- x = start.origin.x + (start.size.width - width) / 2;
374
- }
375
- if (anchor.y === "top") {
376
- y = anchorY;
377
- } else if (anchor.y === "bottom") {
378
- y = anchorY - height;
379
- } else {
380
- y = start.origin.y + (start.size.height - height) / 2;
381
- }
382
- x = Math.max(0, Math.min(x, bbox.width - width));
383
- y = Math.max(0, Math.min(y, bbox.height - height));
384
- return { origin: { x, y }, size: { width, height } };
385
- }
386
- applyConstraints(position) {
387
- const { constraints } = this.config;
388
- if (!constraints) return position;
389
- let {
390
- origin: { x, y },
391
- size: { width, height }
392
- } = position;
393
- const minW = constraints.minWidth ?? 1;
394
- const minH = constraints.minHeight ?? 1;
395
- const maxW = constraints.maxWidth;
396
- const maxH = constraints.maxHeight;
397
- if (this.config.maintainAspectRatio && width > 0 && height > 0) {
398
- const ratio = width / height;
399
- if (width < minW) {
400
- width = minW;
401
- height = width / ratio;
402
- }
403
- if (height < minH) {
404
- height = minH;
405
- width = height * ratio;
406
- }
407
- if (maxW !== void 0 && width > maxW) {
408
- width = maxW;
409
- height = width / ratio;
410
- }
411
- if (maxH !== void 0 && height > maxH) {
412
- height = maxH;
413
- width = height * ratio;
414
- }
415
- } else {
416
- width = Math.max(minW, width);
417
- height = Math.max(minH, height);
418
- if (maxW !== void 0) width = Math.min(maxW, width);
419
- if (maxH !== void 0) height = Math.min(maxH, height);
420
- }
421
- if (constraints.boundingBox) {
422
- x = Math.max(0, Math.min(x, constraints.boundingBox.width - width));
423
- y = Math.max(0, Math.min(y, constraints.boundingBox.height - height));
424
- }
425
- return { origin: { x, y }, size: { width, height } };
860
+ return { angle: normalizedAngle, isSnapped: false };
426
861
  }
427
862
  }
428
- function diagonalCursor(handle, rot) {
429
- const diag0 = {
430
- nw: "nwse-resize",
431
- ne: "nesw-resize",
432
- sw: "nesw-resize",
433
- se: "nwse-resize"
434
- };
435
- if (handle === "n" || handle === "s") return "ns-resize";
436
- if (handle === "e" || handle === "w") return "ew-resize";
437
- if (rot % 2 === 0) return diag0[handle];
438
- return { nw: "nesw-resize", ne: "nwse-resize", sw: "nwse-resize", se: "nesw-resize" }[handle];
439
- }
440
- function edgeOffset(k, spacing, mode) {
441
- const base = -k / 2;
442
- if (mode === "center") return base;
443
- return mode === "outside" ? base - spacing : base + spacing;
444
- }
445
- function describeResizeFromConfig(cfg, ui = {}) {
446
- const {
447
- handleSize = 8,
448
- spacing = 1,
449
- offsetMode = "outside",
450
- includeSides = false,
451
- zIndex = 3,
452
- rotationAwareCursor = true
453
- } = ui;
454
- const rotation = (cfg.pageRotation ?? 0) % 4;
455
- const off = (edge) => ({
456
- [edge]: edgeOffset(handleSize, spacing, offsetMode) + "px"
457
- });
458
- const corners = [
459
- ["nw", { ...off("top"), ...off("left") }],
460
- ["ne", { ...off("top"), ...off("right") }],
461
- ["sw", { ...off("bottom"), ...off("left") }],
462
- ["se", { ...off("bottom"), ...off("right") }]
463
- ];
464
- const sides = includeSides ? [
465
- ["n", { ...off("top"), left: `calc(50% - ${handleSize / 2}px)` }],
466
- ["s", { ...off("bottom"), left: `calc(50% - ${handleSize / 2}px)` }],
467
- ["w", { ...off("left"), top: `calc(50% - ${handleSize / 2}px)` }],
468
- ["e", { ...off("right"), top: `calc(50% - ${handleSize / 2}px)` }]
469
- ] : [];
470
- const all = [...corners, ...sides];
471
- return all.map(([handle, pos]) => ({
472
- handle,
473
- style: {
474
- position: "absolute",
475
- width: handleSize + "px",
476
- height: handleSize + "px",
477
- borderRadius: "50%",
478
- zIndex,
479
- cursor: rotationAwareCursor ? diagonalCursor(handle, rotation) : "default",
480
- touchAction: "none",
481
- ...pos
482
- },
483
- attrs: { "data-epdf-handle": handle }
484
- }));
485
- }
486
- function describeVerticesFromConfig(cfg, ui = {}, liveVertices) {
487
- const { vertexSize = 12, zIndex = 4 } = ui;
488
- const rect = cfg.element;
489
- const scale = cfg.scale ?? 1;
490
- const verts = liveVertices ?? cfg.vertices ?? [];
491
- return verts.map((v, i) => {
492
- const left = (v.x - rect.origin.x) * scale - vertexSize / 2;
493
- const top = (v.y - rect.origin.y) * scale - vertexSize / 2;
494
- return {
495
- handle: "nw",
496
- // not used; kept for type
497
- style: {
498
- position: "absolute",
499
- left: left + "px",
500
- top: top + "px",
501
- width: vertexSize + "px",
502
- height: vertexSize + "px",
503
- borderRadius: "50%",
504
- cursor: "pointer",
505
- zIndex,
506
- touchAction: "none"
507
- },
508
- attrs: { "data-epdf-vertex": i }
509
- };
510
- });
511
- }
512
863
  function useDragResize(getOptions) {
513
864
  const config = $.derived(() => {
514
865
  const opts = getOptions();
@@ -540,7 +891,7 @@ function useDragResize(getOptions) {
540
891
  var _a;
541
892
  e.preventDefault();
542
893
  e.stopPropagation();
543
- (_a = $.get(controller)) == null ? void 0 : _a.move(e.clientX, e.clientY);
894
+ (_a = $.get(controller)) == null ? void 0 : _a.move(e.clientX, e.clientY, e.buttons);
544
895
  };
545
896
  const handleEnd = (e) => {
546
897
  var _a, _b, _c;
@@ -575,6 +926,22 @@ function useDragResize(getOptions) {
575
926
  onpointerup: handleEnd,
576
927
  onpointercancel: handleEnd
577
928
  });
929
+ const createRotationHandler = (initialRotation = 0, orbitRadiusPx) => ({
930
+ onpointerdown: (e) => {
931
+ var _a;
932
+ if (!$.get(enabled)) return;
933
+ e.preventDefault();
934
+ e.stopPropagation();
935
+ const handleRect = e.currentTarget.getBoundingClientRect();
936
+ const handleCenterX = handleRect.left + handleRect.width / 2;
937
+ const handleCenterY = handleRect.top + handleRect.height / 2;
938
+ (_a = $.get(controller)) == null ? void 0 : _a.startRotation(handleCenterX, handleCenterY, initialRotation, orbitRadiusPx);
939
+ e.currentTarget.setPointerCapture(e.pointerId);
940
+ },
941
+ onpointermove: handleMove,
942
+ onpointerup: handleEnd,
943
+ onpointercancel: handleEnd
944
+ });
578
945
  const dragProps = $.derived(() => $.get(enabled) ? {
579
946
  onpointerdown: handleDragStart,
580
947
  onpointermove: handleMove,
@@ -586,7 +953,8 @@ function useDragResize(getOptions) {
586
953
  return $.get(dragProps);
587
954
  },
588
955
  createResizeProps: createResizeHandler,
589
- createVertexProps: createVertexHandler
956
+ createVertexProps: createVertexHandler,
957
+ createRotationProps: createRotationHandler
590
958
  };
591
959
  }
592
960
  function stylesToString(style) {
@@ -599,9 +967,13 @@ function useInteractionHandles(getOpts) {
599
967
  const controller = $.derived(() => getOpts().controller);
600
968
  const resizeUI = $.derived(() => getOpts().resizeUI);
601
969
  const vertexUI = $.derived(() => getOpts().vertexUI);
970
+ const rotationUI = $.derived(() => getOpts().rotationUI);
602
971
  const includeVertices = $.derived(() => getOpts().includeVertices ?? false);
972
+ const includeRotation = $.derived(() => getOpts().includeRotation ?? false);
973
+ const currentRotation = $.derived(() => getOpts().currentRotation ?? 0);
603
974
  const handleAttrs = $.derived(() => getOpts().handleAttrs);
604
975
  const vertexAttrs = $.derived(() => getOpts().vertexAttrs);
976
+ const rotationAttrs = $.derived(() => getOpts().rotationAttrs);
605
977
  const dragResize = useDragResize(() => $.get(controller));
606
978
  const resize = $.derived(() => {
607
979
  const desc = describeResizeFromConfig($.get(controller), $.get(resizeUI));
@@ -630,6 +1002,23 @@ function useInteractionHandles(getOpts) {
630
1002
  };
631
1003
  });
632
1004
  });
1005
+ const rotation = $.derived(() => {
1006
+ var _a;
1007
+ if (!$.get(includeRotation)) return null;
1008
+ const desc = describeRotationFromConfig($.get(controller), $.get(rotationUI), $.get(currentRotation));
1009
+ return {
1010
+ handle: {
1011
+ style: stylesToString(desc.handleStyle),
1012
+ ...dragResize.createRotationProps($.get(currentRotation), desc.radius),
1013
+ ...desc.attrs ?? {},
1014
+ ...((_a = $.get(rotationAttrs)) == null ? void 0 : _a()) ?? {}
1015
+ },
1016
+ connector: {
1017
+ style: stylesToString(desc.connectorStyle),
1018
+ "data-epdf-rotation-connector": true
1019
+ }
1020
+ };
1021
+ });
633
1022
  return {
634
1023
  get dragProps() {
635
1024
  return dragResize.dragProps;
@@ -639,6 +1028,9 @@ function useInteractionHandles(getOpts) {
639
1028
  },
640
1029
  get vertices() {
641
1030
  return $.get(vertices);
1031
+ },
1032
+ get rotation() {
1033
+ return $.get(rotation);
642
1034
  }
643
1035
  };
644
1036
  }