@embedpdf/plugin-annotation 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 (71) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +1061 -430
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/annotation-plugin.d.ts +24 -6
  6. package/dist/lib/geometry/index.d.ts +1 -0
  7. package/dist/lib/geometry/rotation.d.ts +32 -0
  8. package/dist/lib/handlers/types.d.ts +3 -1
  9. package/dist/lib/index.d.ts +1 -0
  10. package/dist/lib/patching/base-patch.d.ts +87 -0
  11. package/dist/lib/patching/index.d.ts +1 -0
  12. package/dist/lib/patching/insert-upright.d.ts +20 -0
  13. package/dist/lib/patching/patch-registry.d.ts +14 -1
  14. package/dist/lib/patching/patch-utils.d.ts +54 -1
  15. package/dist/lib/patching/patches/circle.patch.d.ts +3 -0
  16. package/dist/lib/patching/patches/freetext.patch.d.ts +3 -0
  17. package/dist/lib/patching/patches/index.d.ts +4 -0
  18. package/dist/lib/patching/patches/square.patch.d.ts +3 -0
  19. package/dist/lib/patching/patches/stamp.patch.d.ts +3 -0
  20. package/dist/lib/tools/default-tools.d.ts +32 -0
  21. package/dist/lib/tools/types.d.ts +20 -1
  22. package/dist/lib/types.d.ts +67 -3
  23. package/dist/preact/adapter.d.ts +3 -0
  24. package/dist/preact/index.cjs +1 -1
  25. package/dist/preact/index.cjs.map +1 -1
  26. package/dist/preact/index.js +797 -128
  27. package/dist/preact/index.js.map +1 -1
  28. package/dist/react/adapter.d.ts +2 -1
  29. package/dist/react/index.cjs +1 -1
  30. package/dist/react/index.cjs.map +1 -1
  31. package/dist/react/index.js +797 -128
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/shared/components/annotation-container.d.ts +10 -2
  34. package/dist/shared/components/annotation-layer.d.ts +9 -3
  35. package/dist/shared/components/annotations.d.ts +4 -1
  36. package/dist/shared/components/group-selection-box.d.ts +12 -4
  37. package/dist/shared/components/render-annotation.d.ts +2 -1
  38. package/dist/shared/components/types.d.ts +51 -1
  39. package/dist/shared-preact/components/annotation-container.d.ts +10 -2
  40. package/dist/shared-preact/components/annotation-layer.d.ts +9 -3
  41. package/dist/shared-preact/components/annotations.d.ts +4 -1
  42. package/dist/shared-preact/components/group-selection-box.d.ts +12 -4
  43. package/dist/shared-preact/components/render-annotation.d.ts +2 -1
  44. package/dist/shared-preact/components/types.d.ts +51 -1
  45. package/dist/shared-react/components/annotation-container.d.ts +10 -2
  46. package/dist/shared-react/components/annotation-layer.d.ts +9 -3
  47. package/dist/shared-react/components/annotations.d.ts +4 -1
  48. package/dist/shared-react/components/group-selection-box.d.ts +12 -4
  49. package/dist/shared-react/components/render-annotation.d.ts +2 -1
  50. package/dist/shared-react/components/types.d.ts +51 -1
  51. package/dist/svelte/components/AnnotationLayer.svelte.d.ts +8 -2
  52. package/dist/svelte/components/Annotations.svelte.d.ts +7 -1
  53. package/dist/svelte/components/GroupSelectionBox.svelte.d.ts +11 -3
  54. package/dist/svelte/components/RenderAnnotation.svelte.d.ts +1 -0
  55. package/dist/svelte/components/types.d.ts +14 -1
  56. package/dist/svelte/index.cjs +1 -1
  57. package/dist/svelte/index.cjs.map +1 -1
  58. package/dist/svelte/index.js +1171 -332
  59. package/dist/svelte/index.js.map +1 -1
  60. package/dist/svelte/types.d.ts +53 -0
  61. package/dist/vue/components/annotation-container.vue.d.ts +35 -9
  62. package/dist/vue/components/annotation-layer.vue.d.ts +29 -5
  63. package/dist/vue/components/annotations.vue.d.ts +278 -134
  64. package/dist/vue/components/group-selection-box.vue.d.ts +35 -10
  65. package/dist/vue/components/render-annotation.vue.d.ts +2 -0
  66. package/dist/vue/index.cjs +1 -1
  67. package/dist/vue/index.cjs.map +1 -1
  68. package/dist/vue/index.js +949 -163
  69. package/dist/vue/index.js.map +1 -1
  70. package/dist/vue/types.d.ts +52 -0
  71. package/package.json +11 -10
@@ -1,11 +1,12 @@
1
1
  import { createPluginPackage } from "@embedpdf/core";
2
- import { AnnotationPlugin, initialDocumentState, patching, getAnnotationsByPageIndex, getSelectedAnnotationIds, resolveInteractionProp, isInk, isSquare, isCircle, isUnderline, isStrikeout, isSquiggly, isHighlight, isLine, isPolyline, isPolygon, isFreeText, isStamp, isLink, AnnotationPluginPackage as AnnotationPluginPackage$1 } from "@embedpdf/plugin-annotation";
2
+ import { initialDocumentState, AnnotationPlugin, patching, getAnnotationsByPageIndex, getSelectedAnnotationIds, resolveInteractionProp, isInk, isSquare, isCircle, isUnderline, isStrikeout, isSquiggly, isHighlight, isLine, isPolyline, isPolygon, isFreeText, isStamp, isLink, AnnotationPluginPackage as AnnotationPluginPackage$1 } from "@embedpdf/plugin-annotation";
3
3
  export * from "@embedpdf/plugin-annotation";
4
4
  import { jsx, jsxs, Fragment } from "preact/jsx-runtime";
5
5
  import { createContext, Fragment as Fragment$1 } from "preact";
6
6
  import { useState, useCallback, useContext, useRef, useEffect, useMemo, useLayoutEffect } from "preact/hooks";
7
- import { usePlugin, useCapability, useDocumentPermissions, useDocumentState } from "@embedpdf/core/preact";
8
- import { boundingRectOrEmpty, PdfAnnotationBorderStyle, textAlignmentToCss, standardFontCss, PdfVerticalAlignment, ignore, PdfErrorCode, blendModeToCss, PdfBlendMode, PdfAnnotationSubtype, Rotation } from "@embedpdf/models";
7
+ import { createPortal } from "preact/compat";
8
+ import { useCapability, usePlugin, useDocumentPermissions, useDocumentState } from "@embedpdf/core/preact";
9
+ import { inferRotationCenterFromRects, boundingRectOrEmpty, PdfAnnotationBorderStyle, PdfVerticalAlignment, textAlignmentToCss, standardFontCssProperties, ignore, PdfErrorCode, blendModeToCss, PdfBlendMode, PdfAnnotationSubtype } from "@embedpdf/models";
9
10
  import { usePointerHandlers } from "@embedpdf/plugin-interaction-manager/preact";
10
11
  import { useSelectionCapability } from "@embedpdf/plugin-selection/preact";
11
12
  import { useInteractionHandles, useDoublePressProps, CounterRotate } from "@embedpdf/utils/preact";
@@ -71,6 +72,7 @@ function AnnotationContainer({
71
72
  isMultiSelected = false,
72
73
  isDraggable,
73
74
  isResizable,
75
+ isRotatable = true,
74
76
  lockAspectRatio = false,
75
77
  style = {},
76
78
  vertexConfig,
@@ -81,20 +83,28 @@ function AnnotationContainer({
81
83
  zIndex = 1,
82
84
  resizeUI,
83
85
  vertexUI,
84
- selectionOutlineColor = "#007ACC",
86
+ rotationUI,
87
+ selectionOutlineColor,
88
+ selectionOutline,
85
89
  customAnnotationRenderer,
86
90
  // Destructure props that shouldn't be passed to DOM elements
87
91
  groupSelectionMenu: _groupSelectionMenu,
92
+ groupSelectionOutline: _groupSelectionOutline,
88
93
  annotationRenderers: _annotationRenderers,
89
94
  ...props
90
95
  }) {
96
+ var _a, _b, _c;
91
97
  const [preview, setPreview] = useState(trackedAnnotation.object);
98
+ const [liveRotation, setLiveRotation] = useState(null);
99
+ const [cursorScreen, setCursorScreen] = useState(null);
100
+ const [isHandleHovered, setIsHandleHovered] = useState(false);
92
101
  const { provides: annotationCapability } = useAnnotationCapability();
93
102
  const { plugin } = useAnnotationPlugin();
94
103
  const { canModifyAnnotations } = useDocumentPermissions(documentId);
95
104
  const gestureBaseRef = useRef(null);
96
105
  const effectiveIsDraggable = canModifyAnnotations && isDraggable && !isMultiSelected;
97
106
  const effectiveIsResizable = canModifyAnnotations && isResizable && !isMultiSelected;
107
+ const effectiveIsRotatable = canModifyAnnotations && isRotatable && !isMultiSelected;
98
108
  const annotationProvides = useMemo(
99
109
  () => annotationCapability ? annotationCapability.forDocument(documentId) : null,
100
110
  [annotationCapability, documentId]
@@ -102,18 +112,35 @@ function AnnotationContainer({
102
112
  const currentObject = preview ? { ...trackedAnnotation.object, ...preview } : trackedAnnotation.object;
103
113
  const HANDLE_COLOR = (resizeUI == null ? void 0 : resizeUI.color) ?? "#007ACC";
104
114
  const VERTEX_COLOR = (vertexUI == null ? void 0 : vertexUI.color) ?? "#007ACC";
115
+ const ROTATION_COLOR = (rotationUI == null ? void 0 : rotationUI.color) ?? "white";
116
+ const ROTATION_CONNECTOR_COLOR = (rotationUI == null ? void 0 : rotationUI.connectorColor) ?? "#007ACC";
105
117
  const HANDLE_SIZE = (resizeUI == null ? void 0 : resizeUI.size) ?? 12;
106
118
  const VERTEX_SIZE = (vertexUI == null ? void 0 : vertexUI.size) ?? 12;
119
+ const ROTATION_SIZE = (rotationUI == null ? void 0 : rotationUI.size) ?? 32;
120
+ const ROTATION_MARGIN = rotationUI == null ? void 0 : rotationUI.margin;
121
+ const ROTATION_ICON_COLOR = (rotationUI == null ? void 0 : rotationUI.iconColor) ?? "#007ACC";
122
+ const SHOW_CONNECTOR = (rotationUI == null ? void 0 : rotationUI.showConnector) ?? false;
123
+ const ROTATION_BORDER_COLOR = ((_a = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _a.color) ?? "#007ACC";
124
+ const ROTATION_BORDER_WIDTH = ((_b = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _b.width) ?? 1;
125
+ const ROTATION_BORDER_STYLE = ((_c = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _c.style) ?? "solid";
126
+ const outlineColor = (selectionOutline == null ? void 0 : selectionOutline.color) ?? selectionOutlineColor ?? "#007ACC";
127
+ const outlineStyle = (selectionOutline == null ? void 0 : selectionOutline.style) ?? "solid";
128
+ const outlineWidth = (selectionOutline == null ? void 0 : selectionOutline.width) ?? 1;
129
+ const outlineOff = (selectionOutline == null ? void 0 : selectionOutline.offset) ?? outlineOffset ?? 1;
130
+ const annotationRotation = liveRotation ?? currentObject.rotation ?? 0;
131
+ const rotationDisplay = liveRotation ?? currentObject.rotation ?? 0;
132
+ const normalizedRotationDisplay = Number.isFinite(rotationDisplay) ? Math.round(rotationDisplay * 10) / 10 : 0;
133
+ const rotationActive = liveRotation !== null;
107
134
  const gestureBaseRectRef = useRef(null);
108
135
  const handleUpdate = useCallback(
109
136
  (event) => {
110
- var _a;
111
- if (!((_a = event.transformData) == null ? void 0 : _a.type) || isMultiSelected || !plugin) return;
137
+ var _a2;
138
+ if (!((_a2 = event.transformData) == null ? void 0 : _a2.type) || isMultiSelected || !plugin) return;
112
139
  const { type, changes, metadata } = event.transformData;
113
140
  const id = trackedAnnotation.object.id;
114
141
  const pageSize = { width: pageWidth, height: pageHeight };
115
142
  if (event.state === "start") {
116
- gestureBaseRectRef.current = trackedAnnotation.object.rect;
143
+ gestureBaseRectRef.current = trackedAnnotation.object.unrotatedRect ?? trackedAnnotation.object.rect;
117
144
  gestureBaseRef.current = trackedAnnotation.object;
118
145
  if (type === "move") {
119
146
  plugin.startDrag(documentId, { annotationIds: [id], pageSize });
@@ -151,6 +178,27 @@ function AnnotationContainer({
151
178
  }
152
179
  }
153
180
  }
181
+ if (type === "rotate") {
182
+ const cursorAngle = (metadata == null ? void 0 : metadata.rotationAngle) ?? annotationRotation;
183
+ const cursorPos = metadata == null ? void 0 : metadata.cursorPosition;
184
+ if (cursorPos) setCursorScreen({ x: cursorPos.clientX, y: cursorPos.clientY });
185
+ if (event.state === "start") {
186
+ setLiveRotation(cursorAngle);
187
+ plugin.startRotation(documentId, {
188
+ annotationIds: [id],
189
+ cursorAngle,
190
+ rotationCenter: metadata == null ? void 0 : metadata.rotationCenter
191
+ });
192
+ } else if (event.state === "move") {
193
+ setLiveRotation(cursorAngle);
194
+ plugin.updateRotation(documentId, cursorAngle, metadata == null ? void 0 : metadata.rotationDelta);
195
+ } else if (event.state === "end") {
196
+ setLiveRotation(null);
197
+ setCursorScreen(null);
198
+ plugin.commitRotation(documentId);
199
+ }
200
+ return;
201
+ }
154
202
  if (event.state === "end") {
155
203
  gestureBaseRectRef.current = null;
156
204
  gestureBaseRef.current = null;
@@ -168,12 +216,22 @@ function AnnotationContainer({
168
216
  isMultiSelected,
169
217
  vertexConfig,
170
218
  annotationCapability,
171
- annotationProvides
219
+ annotationProvides,
220
+ annotationRotation
172
221
  ]
173
222
  );
174
- const { dragProps, vertices, resize } = useInteractionHandles({
223
+ const explicitUnrotatedRect = currentObject.unrotatedRect;
224
+ const effectiveUnrotatedRect = explicitUnrotatedRect ?? currentObject.rect;
225
+ const rotationPivot = explicitUnrotatedRect && annotationRotation !== 0 ? inferRotationCenterFromRects(effectiveUnrotatedRect, currentObject.rect, annotationRotation) : void 0;
226
+ const controllerElement = effectiveUnrotatedRect;
227
+ const {
228
+ dragProps,
229
+ vertices,
230
+ resize,
231
+ rotation: rotationHandle
232
+ } = useInteractionHandles({
175
233
  controller: {
176
- element: currentObject.rect,
234
+ element: controllerElement,
177
235
  vertices: vertexConfig == null ? void 0 : vertexConfig.extractVertices(currentObject),
178
236
  constraints: {
179
237
  minWidth: 10,
@@ -182,6 +240,9 @@ function AnnotationContainer({
182
240
  },
183
241
  maintainAspectRatio: lockAspectRatio,
184
242
  pageRotation: rotation,
243
+ annotationRotation,
244
+ rotationCenter: rotationPivot,
245
+ rotationElement: currentObject.rect,
185
246
  scale,
186
247
  // Disable interaction handles when multi-selected
187
248
  enabled: isSelected && !isMultiSelected,
@@ -189,7 +250,7 @@ function AnnotationContainer({
189
250
  },
190
251
  resizeUI: {
191
252
  handleSize: HANDLE_SIZE,
192
- spacing: outlineOffset,
253
+ spacing: outlineOff,
193
254
  offsetMode: "outside",
194
255
  includeSides: lockAspectRatio ? false : true,
195
256
  zIndex: zIndex + 1
@@ -198,7 +259,15 @@ function AnnotationContainer({
198
259
  vertexSize: VERTEX_SIZE,
199
260
  zIndex: zIndex + 2
200
261
  },
201
- includeVertices: vertexConfig ? true : false
262
+ rotationUI: {
263
+ handleSize: ROTATION_SIZE,
264
+ margin: ROTATION_MARGIN,
265
+ zIndex: zIndex + 3,
266
+ showConnector: SHOW_CONNECTOR
267
+ },
268
+ includeVertices: vertexConfig ? true : false,
269
+ includeRotation: effectiveIsRotatable,
270
+ currentRotation: annotationRotation
202
271
  });
203
272
  const guardedOnDoubleClick = useMemo(() => {
204
273
  if (!canModifyAnnotations || !onDoubleClick) return void 0;
@@ -212,88 +281,268 @@ function AnnotationContainer({
212
281
  if (!plugin) return;
213
282
  const id = trackedAnnotation.object.id;
214
283
  const handleEvent = (event) => {
215
- var _a;
284
+ var _a2;
216
285
  if (event.documentId !== documentId) return;
217
- const patch = (_a = event.previewPatches) == null ? void 0 : _a[id];
286
+ if (event.type === "end" || event.type === "cancel") {
287
+ setLiveRotation(null);
288
+ }
289
+ const patch = (_a2 = event.previewPatches) == null ? void 0 : _a2[id];
218
290
  if (event.type === "update" && patch) setPreview((prev) => ({ ...prev, ...patch }));
219
291
  else if (event.type === "cancel") setPreview(trackedAnnotation.object);
220
292
  };
221
- const unsubs = [plugin.onDragChange(handleEvent), plugin.onResizeChange(handleEvent)];
293
+ const unsubs = [
294
+ plugin.onDragChange(handleEvent),
295
+ plugin.onResizeChange(handleEvent),
296
+ plugin.onRotateChange(handleEvent)
297
+ ];
222
298
  return () => unsubs.forEach((u) => u());
223
299
  }, [plugin, documentId, trackedAnnotation.object]);
224
300
  const showOutline = isSelected && !isMultiSelected;
301
+ const aabbWidth = currentObject.rect.size.width * scale;
302
+ const aabbHeight = currentObject.rect.size.height * scale;
303
+ const innerWidth = effectiveUnrotatedRect.size.width * scale;
304
+ const innerHeight = effectiveUnrotatedRect.size.height * scale;
305
+ const usesCustomPivot = Boolean(explicitUnrotatedRect) && annotationRotation !== 0;
306
+ const innerLeft = usesCustomPivot ? (effectiveUnrotatedRect.origin.x - currentObject.rect.origin.x) * scale : (aabbWidth - innerWidth) / 2;
307
+ const innerTop = usesCustomPivot ? (effectiveUnrotatedRect.origin.y - currentObject.rect.origin.y) * scale : (aabbHeight - innerHeight) / 2;
308
+ const innerTransformOrigin = usesCustomPivot && rotationPivot ? `${(rotationPivot.x - effectiveUnrotatedRect.origin.x) * scale}px ${(rotationPivot.y - effectiveUnrotatedRect.origin.y) * scale}px` : "center center";
309
+ const centerX = rotationPivot ? (rotationPivot.x - currentObject.rect.origin.x) * scale : aabbWidth / 2;
310
+ const centerY = rotationPivot ? (rotationPivot.y - currentObject.rect.origin.y) * scale : aabbHeight / 2;
311
+ const guideLength = Math.max(300, Math.max(aabbWidth, aabbHeight) + 80);
312
+ const childObject = useMemo(() => {
313
+ if (explicitUnrotatedRect) {
314
+ return { ...currentObject, rect: explicitUnrotatedRect };
315
+ }
316
+ return currentObject;
317
+ }, [currentObject, explicitUnrotatedRect]);
225
318
  return /* @__PURE__ */ jsxs("div", { "data-no-interaction": true, children: [
226
319
  /* @__PURE__ */ jsxs(
227
320
  "div",
228
321
  {
229
- ...effectiveIsDraggable && isSelected ? dragProps : {},
230
- ...doubleProps,
231
322
  style: {
232
323
  position: "absolute",
233
324
  left: currentObject.rect.origin.x * scale,
234
325
  top: currentObject.rect.origin.y * scale,
235
- width: currentObject.rect.size.width * scale,
236
- height: currentObject.rect.size.height * scale,
237
- outline: showOutline ? `1px solid ${selectionOutlineColor}` : "none",
238
- outlineOffset: showOutline ? `${outlineOffset}px` : "0px",
239
- pointerEvents: isSelected && !isMultiSelected ? "auto" : "none",
240
- touchAction: "none",
241
- cursor: isSelected && effectiveIsDraggable ? "move" : "default",
326
+ width: aabbWidth,
327
+ height: aabbHeight,
328
+ pointerEvents: "none",
242
329
  zIndex,
243
330
  ...style
244
331
  },
245
332
  ...props,
246
333
  children: [
247
- (() => {
248
- const childrenRender = typeof children === "function" ? children(currentObject) : children;
249
- const customRender = customAnnotationRenderer == null ? void 0 : customAnnotationRenderer({
250
- annotation: currentObject,
251
- children: childrenRender,
252
- isSelected,
253
- scale,
254
- rotation,
255
- pageWidth,
256
- pageHeight,
257
- pageIndex,
258
- onSelect
259
- });
260
- if (customRender !== null && customRender !== void 0) {
261
- return customRender;
262
- }
263
- return childrenRender;
264
- })(),
265
- isSelected && effectiveIsResizable && resize.map(
266
- ({ key, ...hProps }) => (resizeUI == null ? void 0 : resizeUI.component) ? resizeUI.component({
267
- key,
268
- ...hProps,
269
- backgroundColor: HANDLE_COLOR
270
- }) : /* @__PURE__ */ jsx(
334
+ rotationActive && /* @__PURE__ */ jsxs(Fragment, { children: [
335
+ /* @__PURE__ */ jsx(
271
336
  "div",
272
337
  {
273
- ...hProps,
274
- style: { ...hProps.style, backgroundColor: HANDLE_COLOR }
275
- },
276
- key
277
- )
278
- ),
279
- isSelected && canModifyAnnotations && !isMultiSelected && vertices.map(
280
- ({ key, ...vProps }) => (vertexUI == null ? void 0 : vertexUI.component) ? vertexUI.component({
281
- key,
282
- ...vProps,
283
- backgroundColor: VERTEX_COLOR
284
- }) : /* @__PURE__ */ jsx(
338
+ style: {
339
+ position: "absolute",
340
+ left: centerX - guideLength / 2,
341
+ top: centerY,
342
+ width: guideLength,
343
+ height: 1,
344
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
345
+ opacity: 0.35,
346
+ pointerEvents: "none"
347
+ }
348
+ }
349
+ ),
350
+ /* @__PURE__ */ jsx(
285
351
  "div",
286
352
  {
287
- ...vProps,
288
- style: { ...vProps.style, backgroundColor: VERTEX_COLOR }
289
- },
290
- key
353
+ style: {
354
+ position: "absolute",
355
+ left: centerX,
356
+ top: centerY - guideLength / 2,
357
+ width: 1,
358
+ height: guideLength,
359
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
360
+ opacity: 0.35,
361
+ pointerEvents: "none"
362
+ }
363
+ }
364
+ ),
365
+ /* @__PURE__ */ jsx(
366
+ "div",
367
+ {
368
+ style: {
369
+ position: "absolute",
370
+ left: centerX - guideLength / 2,
371
+ top: centerY,
372
+ width: guideLength,
373
+ height: 1,
374
+ transformOrigin: "center center",
375
+ transform: `rotate(${annotationRotation}deg)`,
376
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
377
+ opacity: 0.8,
378
+ pointerEvents: "none"
379
+ }
380
+ }
291
381
  )
382
+ ] }),
383
+ isSelected && effectiveIsRotatable && rotationHandle && ((rotationUI == null ? void 0 : rotationUI.component) ? /* @__PURE__ */ jsx(
384
+ "div",
385
+ {
386
+ onPointerEnter: () => setIsHandleHovered(true),
387
+ onPointerLeave: () => {
388
+ setIsHandleHovered(false);
389
+ setCursorScreen(null);
390
+ },
391
+ onPointerMove: (e) => {
392
+ if (!rotationActive) setCursorScreen({ x: e.clientX, y: e.clientY });
393
+ },
394
+ style: { display: "contents" },
395
+ children: rotationUI.component({
396
+ ...rotationHandle.handle,
397
+ backgroundColor: ROTATION_COLOR,
398
+ iconColor: ROTATION_ICON_COLOR,
399
+ connectorStyle: {
400
+ ...rotationHandle.connector.style,
401
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
402
+ opacity: rotationActive ? 0 : 1
403
+ },
404
+ showConnector: SHOW_CONNECTOR,
405
+ opacity: rotationActive ? 0 : 1,
406
+ border: {
407
+ color: ROTATION_BORDER_COLOR,
408
+ width: ROTATION_BORDER_WIDTH,
409
+ style: ROTATION_BORDER_STYLE
410
+ }
411
+ })
412
+ }
413
+ ) : /* @__PURE__ */ jsxs(
414
+ "div",
415
+ {
416
+ onPointerEnter: () => setIsHandleHovered(true),
417
+ onPointerLeave: () => {
418
+ setIsHandleHovered(false);
419
+ setCursorScreen(null);
420
+ },
421
+ onPointerMove: (e) => {
422
+ if (!rotationActive) setCursorScreen({ x: e.clientX, y: e.clientY });
423
+ },
424
+ style: { display: "contents" },
425
+ children: [
426
+ SHOW_CONNECTOR && /* @__PURE__ */ jsx(
427
+ "div",
428
+ {
429
+ style: {
430
+ ...rotationHandle.connector.style,
431
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
432
+ opacity: rotationActive ? 0 : 1
433
+ }
434
+ }
435
+ ),
436
+ /* @__PURE__ */ jsx(
437
+ "div",
438
+ {
439
+ ...rotationHandle.handle,
440
+ style: {
441
+ ...rotationHandle.handle.style,
442
+ backgroundColor: ROTATION_COLOR,
443
+ border: `${ROTATION_BORDER_WIDTH}px ${ROTATION_BORDER_STYLE} ${ROTATION_BORDER_COLOR}`,
444
+ boxSizing: "border-box",
445
+ display: "flex",
446
+ alignItems: "center",
447
+ justifyContent: "center",
448
+ pointerEvents: "auto",
449
+ opacity: rotationActive ? 0 : 1
450
+ },
451
+ children: /* @__PURE__ */ jsxs(
452
+ "svg",
453
+ {
454
+ width: Math.round(ROTATION_SIZE * 0.6),
455
+ height: Math.round(ROTATION_SIZE * 0.6),
456
+ viewBox: "0 0 24 24",
457
+ fill: "none",
458
+ stroke: ROTATION_ICON_COLOR,
459
+ strokeWidth: "2",
460
+ strokeLinecap: "round",
461
+ strokeLinejoin: "round",
462
+ children: [
463
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" }),
464
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" })
465
+ ]
466
+ }
467
+ )
468
+ }
469
+ )
470
+ ]
471
+ }
472
+ )),
473
+ /* @__PURE__ */ jsxs(
474
+ "div",
475
+ {
476
+ ...effectiveIsDraggable && isSelected ? dragProps : {},
477
+ ...doubleProps,
478
+ style: {
479
+ position: "absolute",
480
+ left: innerLeft,
481
+ top: innerTop,
482
+ width: innerWidth,
483
+ height: innerHeight,
484
+ transform: annotationRotation !== 0 ? `rotate(${annotationRotation}deg)` : void 0,
485
+ transformOrigin: innerTransformOrigin,
486
+ outline: showOutline ? `${outlineWidth}px ${outlineStyle} ${outlineColor}` : "none",
487
+ outlineOffset: showOutline ? `${outlineOff}px` : "0px",
488
+ pointerEvents: isSelected && !isMultiSelected ? "auto" : "none",
489
+ touchAction: "none",
490
+ cursor: isSelected && effectiveIsDraggable ? "move" : "default"
491
+ },
492
+ children: [
493
+ (() => {
494
+ const childrenRender = typeof children === "function" ? children(childObject) : children;
495
+ const customRender = customAnnotationRenderer == null ? void 0 : customAnnotationRenderer({
496
+ annotation: childObject,
497
+ children: childrenRender,
498
+ isSelected,
499
+ scale,
500
+ rotation,
501
+ pageWidth,
502
+ pageHeight,
503
+ pageIndex,
504
+ onSelect
505
+ });
506
+ if (customRender !== null && customRender !== void 0) {
507
+ return customRender;
508
+ }
509
+ return childrenRender;
510
+ })(),
511
+ isSelected && effectiveIsResizable && !rotationActive && resize.map(
512
+ ({ key, ...hProps }) => (resizeUI == null ? void 0 : resizeUI.component) ? resizeUI.component({
513
+ key,
514
+ ...hProps,
515
+ backgroundColor: HANDLE_COLOR
516
+ }) : /* @__PURE__ */ jsx(
517
+ "div",
518
+ {
519
+ ...hProps,
520
+ style: { ...hProps.style, backgroundColor: HANDLE_COLOR }
521
+ },
522
+ key
523
+ )
524
+ ),
525
+ isSelected && canModifyAnnotations && !isMultiSelected && !rotationActive && vertices.map(
526
+ ({ key, ...vProps }) => (vertexUI == null ? void 0 : vertexUI.component) ? vertexUI.component({
527
+ key,
528
+ ...vProps,
529
+ backgroundColor: VERTEX_COLOR
530
+ }) : /* @__PURE__ */ jsx(
531
+ "div",
532
+ {
533
+ ...vProps,
534
+ style: { ...vProps.style, backgroundColor: VERTEX_COLOR }
535
+ },
536
+ key
537
+ )
538
+ )
539
+ ]
540
+ }
292
541
  )
293
542
  ]
294
543
  }
295
544
  ),
296
- selectionMenu && !isMultiSelected && /* @__PURE__ */ jsx(
545
+ selectionMenu && !isMultiSelected && !rotationActive && /* @__PURE__ */ jsx(
297
546
  CounterRotate,
298
547
  {
299
548
  rect: {
@@ -307,19 +556,49 @@ function AnnotationContainer({
307
556
  }
308
557
  },
309
558
  rotation,
310
- children: (counterRotateProps) => selectionMenu({
311
- ...counterRotateProps,
312
- context: {
313
- type: "annotation",
314
- annotation: trackedAnnotation,
315
- pageIndex
316
- },
317
- selected: isSelected,
318
- placement: {
319
- suggestTop: false
320
- }
321
- })
559
+ children: (counterRotateProps) => {
560
+ const effectiveAngle = ((annotationRotation + rotation * 90) % 360 + 360) % 360;
561
+ const handleNearMenuSide = effectiveIsRotatable && effectiveAngle > 90 && effectiveAngle < 270;
562
+ return selectionMenu({
563
+ ...counterRotateProps,
564
+ context: {
565
+ type: "annotation",
566
+ annotation: trackedAnnotation,
567
+ pageIndex
568
+ },
569
+ selected: isSelected,
570
+ placement: {
571
+ suggestTop: handleNearMenuSide
572
+ }
573
+ });
574
+ }
322
575
  }
576
+ ),
577
+ (rotationActive || isHandleHovered) && cursorScreen && createPortal(
578
+ /* @__PURE__ */ jsxs(
579
+ "div",
580
+ {
581
+ style: {
582
+ position: "fixed",
583
+ left: cursorScreen.x + 16,
584
+ top: cursorScreen.y - 16,
585
+ background: "rgba(0,0,0,0.8)",
586
+ color: "#fff",
587
+ padding: "4px 8px",
588
+ borderRadius: 4,
589
+ fontSize: 12,
590
+ fontFamily: "monospace",
591
+ pointerEvents: "none",
592
+ zIndex: 1e4,
593
+ whiteSpace: "nowrap"
594
+ },
595
+ children: [
596
+ normalizedRotationDisplay.toFixed(0),
597
+ "°"
598
+ ]
599
+ }
600
+ ),
601
+ document.body
323
602
  )
324
603
  ] });
325
604
  }
@@ -333,19 +612,28 @@ function GroupSelectionBox({
333
612
  selectedAnnotations,
334
613
  isDraggable,
335
614
  isResizable,
615
+ isRotatable = true,
616
+ lockAspectRatio = false,
336
617
  resizeUI,
337
- selectionOutlineColor = "#007ACC",
338
- outlineOffset = 2,
339
- zIndex = 100,
618
+ rotationUI,
619
+ selectionOutlineColor,
620
+ outlineOffset,
621
+ selectionOutline,
622
+ zIndex = 2,
340
623
  groupSelectionMenu
341
624
  }) {
625
+ var _a, _b, _c;
342
626
  const { plugin } = useAnnotationPlugin();
343
627
  const { canModifyAnnotations } = useDocumentPermissions(documentId);
344
628
  const gestureBaseRef = useRef(null);
345
629
  const isDraggingRef = useRef(false);
346
630
  const isResizingRef = useRef(false);
631
+ const [liveRotation, setLiveRotation] = useState(null);
632
+ const [cursorScreen, setCursorScreen] = useState(null);
633
+ const [isHandleHovered, setIsHandleHovered] = useState(false);
347
634
  const effectiveIsDraggable = canModifyAnnotations && isDraggable;
348
635
  const effectiveIsResizable = canModifyAnnotations && isResizable;
636
+ const effectiveIsRotatable = canModifyAnnotations && isRotatable;
349
637
  const groupBox = useMemo(() => {
350
638
  const rects = selectedAnnotations.map((ta) => ta.object.rect);
351
639
  return boundingRectOrEmpty(rects);
@@ -356,10 +644,20 @@ function GroupSelectionBox({
356
644
  setPreviewGroupBox(groupBox);
357
645
  }
358
646
  }, [groupBox]);
647
+ useEffect(() => {
648
+ if (!plugin) return;
649
+ const unsubscribe = plugin.onRotateChange((event) => {
650
+ if (event.documentId !== documentId) return;
651
+ if (event.type === "end" || event.type === "cancel") {
652
+ setLiveRotation(null);
653
+ }
654
+ });
655
+ return unsubscribe;
656
+ }, [plugin, documentId]);
359
657
  const handleUpdate = useCallback(
360
658
  (event) => {
361
- var _a, _b;
362
- if (!((_a = event.transformData) == null ? void 0 : _a.type)) return;
659
+ var _a2, _b2, _c2, _d, _e, _f;
660
+ if (!((_a2 = event.transformData) == null ? void 0 : _a2.type)) return;
363
661
  if (!plugin) return;
364
662
  const transformType = event.transformData.type;
365
663
  const isMove = transformType === "move";
@@ -378,10 +676,37 @@ function GroupSelectionBox({
378
676
  plugin.startResize(documentId, {
379
677
  annotationIds: selectedAnnotations.map((ta) => ta.object.id),
380
678
  pageSize: { width: pageWidth, height: pageHeight },
381
- resizeHandle: ((_b = event.transformData.metadata) == null ? void 0 : _b.handle) ?? "se"
679
+ resizeHandle: ((_b2 = event.transformData.metadata) == null ? void 0 : _b2.handle) ?? "se"
382
680
  });
383
681
  }
384
682
  }
683
+ if (transformType === "rotate") {
684
+ if (!isRotatable) return;
685
+ const ids = selectedAnnotations.map((ta) => ta.object.id);
686
+ const cursorAngle = ((_c2 = event.transformData.metadata) == null ? void 0 : _c2.rotationAngle) ?? 0;
687
+ const cursorPos = (_d = event.transformData.metadata) == null ? void 0 : _d.cursorPosition;
688
+ if (cursorPos) setCursorScreen({ x: cursorPos.clientX, y: cursorPos.clientY });
689
+ if (event.state === "start") {
690
+ setLiveRotation(cursorAngle);
691
+ plugin.startRotation(documentId, {
692
+ annotationIds: ids,
693
+ cursorAngle,
694
+ rotationCenter: (_e = event.transformData.metadata) == null ? void 0 : _e.rotationCenter
695
+ });
696
+ } else if (event.state === "move") {
697
+ setLiveRotation(cursorAngle);
698
+ plugin.updateRotation(
699
+ documentId,
700
+ cursorAngle,
701
+ (_f = event.transformData.metadata) == null ? void 0 : _f.rotationDelta
702
+ );
703
+ } else if (event.state === "end") {
704
+ setLiveRotation(null);
705
+ setCursorScreen(null);
706
+ plugin.commitRotation(documentId);
707
+ }
708
+ return;
709
+ }
385
710
  const base = gestureBaseRef.current ?? groupBox;
386
711
  if (isMove && event.transformData.changes.rect) {
387
712
  const newRect = event.transformData.changes.rect;
@@ -420,12 +745,33 @@ function GroupSelectionBox({
420
745
  pageHeight,
421
746
  groupBox,
422
747
  effectiveIsDraggable,
423
- selectedAnnotations
748
+ selectedAnnotations,
749
+ isRotatable
424
750
  ]
425
751
  );
752
+ const groupRotationDisplay = liveRotation ?? 0;
753
+ const rotationActive = liveRotation !== null;
754
+ const normalizedRotationDisplay = Number.isFinite(groupRotationDisplay) ? Math.round(groupRotationDisplay * 10) / 10 : 0;
426
755
  const HANDLE_COLOR = (resizeUI == null ? void 0 : resizeUI.color) ?? "#007ACC";
427
756
  const HANDLE_SIZE = (resizeUI == null ? void 0 : resizeUI.size) ?? 12;
428
- const { dragProps, resize } = useInteractionHandles({
757
+ const ROTATION_COLOR = (rotationUI == null ? void 0 : rotationUI.color) ?? "white";
758
+ const ROTATION_CONNECTOR_COLOR = (rotationUI == null ? void 0 : rotationUI.connectorColor) ?? "#007ACC";
759
+ const ROTATION_SIZE = (rotationUI == null ? void 0 : rotationUI.size) ?? 32;
760
+ const ROTATION_MARGIN = rotationUI == null ? void 0 : rotationUI.margin;
761
+ const ROTATION_ICON_COLOR = (rotationUI == null ? void 0 : rotationUI.iconColor) ?? "#007ACC";
762
+ const SHOW_CONNECTOR = (rotationUI == null ? void 0 : rotationUI.showConnector) ?? false;
763
+ const ROTATION_BORDER_COLOR = ((_a = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _a.color) ?? "#007ACC";
764
+ const ROTATION_BORDER_WIDTH = ((_b = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _b.width) ?? 1;
765
+ const ROTATION_BORDER_STYLE = ((_c = rotationUI == null ? void 0 : rotationUI.border) == null ? void 0 : _c.style) ?? "solid";
766
+ const outlineColor = (selectionOutline == null ? void 0 : selectionOutline.color) ?? selectionOutlineColor ?? "#007ACC";
767
+ const outlineStyleVal = (selectionOutline == null ? void 0 : selectionOutline.style) ?? "dashed";
768
+ const outlineWidth = (selectionOutline == null ? void 0 : selectionOutline.width) ?? 2;
769
+ const outlineOff = (selectionOutline == null ? void 0 : selectionOutline.offset) ?? outlineOffset ?? 2;
770
+ const {
771
+ dragProps,
772
+ resize,
773
+ rotation: rotationHandle
774
+ } = useInteractionHandles({
429
775
  controller: {
430
776
  element: previewGroupBox,
431
777
  constraints: {
@@ -433,7 +779,7 @@ function GroupSelectionBox({
433
779
  minHeight: 20,
434
780
  boundingBox: { width: pageWidth, height: pageHeight }
435
781
  },
436
- maintainAspectRatio: false,
782
+ maintainAspectRatio: lockAspectRatio,
437
783
  pageRotation: rotation,
438
784
  scale,
439
785
  enabled: true,
@@ -441,55 +787,249 @@ function GroupSelectionBox({
441
787
  },
442
788
  resizeUI: {
443
789
  handleSize: HANDLE_SIZE,
444
- spacing: outlineOffset,
790
+ spacing: outlineOff,
445
791
  offsetMode: "outside",
446
- includeSides: true,
792
+ includeSides: !lockAspectRatio,
447
793
  zIndex: zIndex + 1
448
794
  },
449
795
  vertexUI: {
450
796
  vertexSize: 0,
451
797
  zIndex
452
798
  },
453
- includeVertices: false
799
+ rotationUI: {
800
+ handleSize: ROTATION_SIZE,
801
+ margin: ROTATION_MARGIN,
802
+ zIndex: zIndex + 2,
803
+ showConnector: SHOW_CONNECTOR
804
+ },
805
+ includeVertices: false,
806
+ includeRotation: effectiveIsRotatable,
807
+ currentRotation: liveRotation ?? 0
454
808
  });
455
809
  if (selectedAnnotations.length < 2) {
456
810
  return null;
457
811
  }
812
+ const groupBoxWidth = previewGroupBox.size.width * scale;
813
+ const groupBoxHeight = previewGroupBox.size.height * scale;
814
+ const groupCenterX = groupBoxWidth / 2;
815
+ const groupCenterY = groupBoxHeight / 2;
816
+ const groupGuideLength = Math.max(300, Math.max(groupBoxWidth, groupBoxHeight) + 80);
458
817
  return /* @__PURE__ */ jsxs("div", { "data-group-selection-box": true, "data-no-interaction": true, children: [
459
- /* @__PURE__ */ jsx(
818
+ /* @__PURE__ */ jsxs(
460
819
  "div",
461
820
  {
462
- ...effectiveIsDraggable ? dragProps : {
463
- onPointerDown: (e) => e.stopPropagation()
464
- },
465
821
  style: {
466
822
  position: "absolute",
467
823
  left: previewGroupBox.origin.x * scale,
468
824
  top: previewGroupBox.origin.y * scale,
469
- width: previewGroupBox.size.width * scale,
470
- height: previewGroupBox.size.height * scale,
471
- outline: `2px dashed ${selectionOutlineColor}`,
472
- outlineOffset: outlineOffset - 1,
473
- cursor: effectiveIsDraggable ? "move" : "default",
474
- touchAction: "none",
825
+ width: groupBoxWidth,
826
+ height: groupBoxHeight,
827
+ pointerEvents: "none",
475
828
  zIndex
476
829
  },
477
- children: effectiveIsResizable && resize.map(
478
- ({ key, ...hProps }) => (resizeUI == null ? void 0 : resizeUI.component) ? resizeUI.component({
479
- key,
480
- ...hProps,
481
- backgroundColor: HANDLE_COLOR
482
- }) : /* @__PURE__ */ jsx(
830
+ children: [
831
+ rotationActive && /* @__PURE__ */ jsxs(Fragment, { children: [
832
+ /* @__PURE__ */ jsx(
833
+ "div",
834
+ {
835
+ style: {
836
+ position: "absolute",
837
+ left: groupCenterX - groupGuideLength / 2,
838
+ top: groupCenterY,
839
+ width: groupGuideLength,
840
+ height: 1,
841
+ backgroundColor: HANDLE_COLOR,
842
+ opacity: 0.35,
843
+ pointerEvents: "none"
844
+ }
845
+ }
846
+ ),
847
+ /* @__PURE__ */ jsx(
848
+ "div",
849
+ {
850
+ style: {
851
+ position: "absolute",
852
+ left: groupCenterX,
853
+ top: groupCenterY - groupGuideLength / 2,
854
+ width: 1,
855
+ height: groupGuideLength,
856
+ backgroundColor: HANDLE_COLOR,
857
+ opacity: 0.35,
858
+ pointerEvents: "none"
859
+ }
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsx(
863
+ "div",
864
+ {
865
+ style: {
866
+ position: "absolute",
867
+ left: groupCenterX - groupGuideLength / 2,
868
+ top: groupCenterY,
869
+ width: groupGuideLength,
870
+ height: 1,
871
+ transformOrigin: "center center",
872
+ transform: `rotate(${groupRotationDisplay}deg)`,
873
+ backgroundColor: HANDLE_COLOR,
874
+ opacity: 0.8,
875
+ pointerEvents: "none"
876
+ }
877
+ }
878
+ )
879
+ ] }),
880
+ effectiveIsRotatable && rotationHandle && ((rotationUI == null ? void 0 : rotationUI.component) ? /* @__PURE__ */ jsx(
483
881
  "div",
484
882
  {
485
- ...hProps,
486
- style: { ...hProps.style, backgroundColor: HANDLE_COLOR }
487
- },
488
- key
883
+ onPointerEnter: () => setIsHandleHovered(true),
884
+ onPointerLeave: () => {
885
+ setIsHandleHovered(false);
886
+ setCursorScreen(null);
887
+ },
888
+ onPointerMove: (e) => {
889
+ if (!rotationActive) setCursorScreen({ x: e.clientX, y: e.clientY });
890
+ },
891
+ style: { display: "contents" },
892
+ children: rotationUI.component({
893
+ ...rotationHandle.handle,
894
+ backgroundColor: ROTATION_COLOR,
895
+ iconColor: ROTATION_ICON_COLOR,
896
+ connectorStyle: {
897
+ ...rotationHandle.connector.style,
898
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
899
+ opacity: rotationActive ? 0 : 1
900
+ },
901
+ showConnector: SHOW_CONNECTOR,
902
+ opacity: rotationActive ? 0 : 1,
903
+ border: {
904
+ color: ROTATION_BORDER_COLOR,
905
+ width: ROTATION_BORDER_WIDTH,
906
+ style: ROTATION_BORDER_STYLE
907
+ }
908
+ })
909
+ }
910
+ ) : /* @__PURE__ */ jsxs(
911
+ "div",
912
+ {
913
+ onPointerEnter: () => setIsHandleHovered(true),
914
+ onPointerLeave: () => {
915
+ setIsHandleHovered(false);
916
+ setCursorScreen(null);
917
+ },
918
+ onPointerMove: (e) => {
919
+ if (!rotationActive) setCursorScreen({ x: e.clientX, y: e.clientY });
920
+ },
921
+ style: { display: "contents" },
922
+ children: [
923
+ SHOW_CONNECTOR && /* @__PURE__ */ jsx(
924
+ "div",
925
+ {
926
+ style: {
927
+ ...rotationHandle.connector.style,
928
+ backgroundColor: ROTATION_CONNECTOR_COLOR,
929
+ opacity: rotationActive ? 0 : 1
930
+ }
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsx(
934
+ "div",
935
+ {
936
+ ...rotationHandle.handle,
937
+ style: {
938
+ ...rotationHandle.handle.style,
939
+ backgroundColor: ROTATION_COLOR,
940
+ border: `${ROTATION_BORDER_WIDTH}px ${ROTATION_BORDER_STYLE} ${ROTATION_BORDER_COLOR}`,
941
+ boxSizing: "border-box",
942
+ display: "flex",
943
+ alignItems: "center",
944
+ justifyContent: "center",
945
+ pointerEvents: "auto",
946
+ opacity: rotationActive ? 0 : 1
947
+ },
948
+ children: /* @__PURE__ */ jsxs(
949
+ "svg",
950
+ {
951
+ width: Math.round(ROTATION_SIZE * 0.6),
952
+ height: Math.round(ROTATION_SIZE * 0.6),
953
+ viewBox: "0 0 24 24",
954
+ fill: "none",
955
+ stroke: ROTATION_ICON_COLOR,
956
+ strokeWidth: "2",
957
+ strokeLinecap: "round",
958
+ strokeLinejoin: "round",
959
+ children: [
960
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" }),
961
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" })
962
+ ]
963
+ }
964
+ )
965
+ }
966
+ )
967
+ ]
968
+ }
969
+ )),
970
+ /* @__PURE__ */ jsx(
971
+ "div",
972
+ {
973
+ ...effectiveIsDraggable ? dragProps : {
974
+ onPointerDown: (e) => e.stopPropagation()
975
+ },
976
+ style: {
977
+ position: "absolute",
978
+ left: 0,
979
+ top: 0,
980
+ width: groupBoxWidth,
981
+ height: groupBoxHeight,
982
+ outline: rotationActive ? "none" : `${outlineWidth}px ${outlineStyleVal} ${outlineColor}`,
983
+ outlineOffset: outlineOff - 1,
984
+ cursor: effectiveIsDraggable ? "move" : "default",
985
+ touchAction: "none",
986
+ pointerEvents: "auto"
987
+ },
988
+ children: effectiveIsResizable && !rotationActive && resize.map(
989
+ ({ key, ...hProps }) => (resizeUI == null ? void 0 : resizeUI.component) ? resizeUI.component({
990
+ key,
991
+ ...hProps,
992
+ backgroundColor: HANDLE_COLOR
993
+ }) : /* @__PURE__ */ jsx(
994
+ "div",
995
+ {
996
+ ...hProps,
997
+ style: { ...hProps.style, backgroundColor: HANDLE_COLOR }
998
+ },
999
+ key
1000
+ )
1001
+ )
1002
+ }
489
1003
  )
490
- )
1004
+ ]
491
1005
  }
492
1006
  ),
1007
+ (rotationActive || isHandleHovered) && cursorScreen && createPortal(
1008
+ /* @__PURE__ */ jsxs(
1009
+ "div",
1010
+ {
1011
+ style: {
1012
+ position: "fixed",
1013
+ left: cursorScreen.x + 16,
1014
+ top: cursorScreen.y - 16,
1015
+ background: "rgba(0,0,0,0.8)",
1016
+ color: "#fff",
1017
+ padding: "4px 8px",
1018
+ borderRadius: 4,
1019
+ fontSize: 12,
1020
+ fontFamily: "monospace",
1021
+ pointerEvents: "none",
1022
+ zIndex: 1e4,
1023
+ whiteSpace: "nowrap"
1024
+ },
1025
+ children: [
1026
+ normalizedRotationDisplay.toFixed(0),
1027
+ "°"
1028
+ ]
1029
+ }
1030
+ ),
1031
+ document.body
1032
+ ),
493
1033
  groupSelectionMenu && /* @__PURE__ */ jsx(
494
1034
  CounterRotate,
495
1035
  {
@@ -504,18 +1044,22 @@ function GroupSelectionBox({
504
1044
  }
505
1045
  },
506
1046
  rotation,
507
- children: (counterRotateProps) => groupSelectionMenu({
508
- ...counterRotateProps,
509
- context: {
510
- type: "group",
511
- annotations: selectedAnnotations,
512
- pageIndex
513
- },
514
- selected: true,
515
- placement: {
516
- suggestTop: false
517
- }
518
- })
1047
+ children: (counterRotateProps) => {
1048
+ const effectiveAngle = ((groupRotationDisplay + rotation * 90) % 360 + 360) % 360;
1049
+ const handleNearMenuSide = effectiveIsRotatable && effectiveAngle > 90 && effectiveAngle < 270;
1050
+ return groupSelectionMenu({
1051
+ ...counterRotateProps,
1052
+ context: {
1053
+ type: "group",
1054
+ annotations: selectedAnnotations,
1055
+ pageIndex
1056
+ },
1057
+ selected: true,
1058
+ placement: {
1059
+ suggestTop: handleNearMenuSide
1060
+ }
1061
+ });
1062
+ }
519
1063
  }
520
1064
  )
521
1065
  ] });
@@ -1284,7 +1828,7 @@ function FreeText({
1284
1828
  style: {
1285
1829
  color: annotation.object.fontColor,
1286
1830
  fontSize: adjustedFontPx,
1287
- fontFamily: standardFontCss(annotation.object.fontFamily),
1831
+ ...standardFontCssProperties(annotation.object.fontFamily),
1288
1832
  textAlign: textAlignmentToCss(annotation.object.textAlign),
1289
1833
  flexDirection: "column",
1290
1834
  justifyContent: annotation.object.verticalAlign === PdfVerticalAlignment.Top ? "flex-start" : annotation.object.verticalAlign === PdfVerticalAlignment.Middle ? "center" : "flex-end",
@@ -1313,6 +1857,7 @@ function RenderAnnotation({
1313
1857
  pageIndex,
1314
1858
  annotation,
1315
1859
  scaleFactor = 1,
1860
+ unrotated,
1316
1861
  style,
1317
1862
  ...props
1318
1863
  }) {
@@ -1327,7 +1872,8 @@ function RenderAnnotation({
1327
1872
  annotation,
1328
1873
  options: {
1329
1874
  scaleFactor,
1330
- dpr: window.devicePixelRatio
1875
+ dpr: window.devicePixelRatio,
1876
+ unrotated
1331
1877
  }
1332
1878
  });
1333
1879
  task.wait((blob) => {
@@ -1347,7 +1893,16 @@ function RenderAnnotation({
1347
1893
  }
1348
1894
  };
1349
1895
  }
1350
- }, [pageIndex, scaleFactor, annotationProvides, documentId, annotation.id, width, height]);
1896
+ }, [
1897
+ pageIndex,
1898
+ scaleFactor,
1899
+ unrotated,
1900
+ annotationProvides,
1901
+ documentId,
1902
+ annotation.id,
1903
+ width,
1904
+ height
1905
+ ]);
1351
1906
  const handleImageLoad = () => {
1352
1907
  if (urlRef.current) {
1353
1908
  URL.revokeObjectURL(urlRef.current);
@@ -1377,6 +1932,7 @@ function Stamp({
1377
1932
  scale,
1378
1933
  onClick
1379
1934
  }) {
1935
+ const unrotated = !!annotation.object.rotation && !!annotation.object.unrotatedRect;
1380
1936
  return /* @__PURE__ */ jsx(
1381
1937
  "div",
1382
1938
  {
@@ -1396,7 +1952,8 @@ function Stamp({
1396
1952
  documentId,
1397
1953
  pageIndex,
1398
1954
  annotation: { ...annotation.object, id: annotation.object.id },
1399
- scaleFactor: scale
1955
+ scaleFactor: scale,
1956
+ unrotated
1400
1957
  }
1401
1958
  )
1402
1959
  }
@@ -1610,6 +2167,40 @@ function Annotations(annotationsProps) {
1610
2167
  return (tool == null ? void 0 : tool.interaction.isGroupResizable) !== void 0 ? groupResizable : singleResizable;
1611
2168
  });
1612
2169
  }, [selectedAnnotationsOnPage, annotationProvides]);
2170
+ const areAllSelectedRotatable = useMemo(() => {
2171
+ if (selectedAnnotationsOnPage.length < 2) return false;
2172
+ return selectedAnnotationsOnPage.every((ta) => {
2173
+ const tool = annotationProvides == null ? void 0 : annotationProvides.findToolForAnnotation(ta.object);
2174
+ const groupRotatable = resolveInteractionProp(
2175
+ tool == null ? void 0 : tool.interaction.isGroupRotatable,
2176
+ ta.object,
2177
+ true
2178
+ );
2179
+ const singleRotatable = resolveInteractionProp(
2180
+ tool == null ? void 0 : tool.interaction.isRotatable,
2181
+ ta.object,
2182
+ true
2183
+ );
2184
+ return (tool == null ? void 0 : tool.interaction.isGroupRotatable) !== void 0 ? groupRotatable : singleRotatable;
2185
+ });
2186
+ }, [selectedAnnotationsOnPage, annotationProvides]);
2187
+ const shouldLockGroupAspectRatio = useMemo(() => {
2188
+ if (selectedAnnotationsOnPage.length < 2) return false;
2189
+ return selectedAnnotationsOnPage.some((ta) => {
2190
+ const tool = annotationProvides == null ? void 0 : annotationProvides.findToolForAnnotation(ta.object);
2191
+ const groupLock = resolveInteractionProp(
2192
+ tool == null ? void 0 : tool.interaction.lockGroupAspectRatio,
2193
+ ta.object,
2194
+ false
2195
+ );
2196
+ const singleLock = resolveInteractionProp(
2197
+ tool == null ? void 0 : tool.interaction.lockAspectRatio,
2198
+ ta.object,
2199
+ false
2200
+ );
2201
+ return (tool == null ? void 0 : tool.interaction.lockGroupAspectRatio) !== void 0 ? groupLock : singleLock;
2202
+ });
2203
+ }, [selectedAnnotationsOnPage, annotationProvides]);
1613
2204
  const allSelectedOnSamePage = useMemo(() => {
1614
2205
  if (!annotationProvides) return false;
1615
2206
  const allSelected = annotationProvides.getSelectedAnnotations();
@@ -1649,6 +2240,11 @@ function Annotations(annotationsProps) {
1649
2240
  annotation.object,
1650
2241
  false
1651
2242
  ),
2243
+ isRotatable: resolveInteractionProp(
2244
+ tool == null ? void 0 : tool.interaction.isRotatable,
2245
+ annotation.object,
2246
+ false
2247
+ ),
1652
2248
  selectionMenu,
1653
2249
  onSelect: (e) => handleClick(e, annotation),
1654
2250
  style: {
@@ -1683,6 +2279,11 @@ function Annotations(annotationsProps) {
1683
2279
  annotation.object,
1684
2280
  false
1685
2281
  ),
2282
+ isRotatable: resolveInteractionProp(
2283
+ tool == null ? void 0 : tool.interaction.isRotatable,
2284
+ annotation.object,
2285
+ true
2286
+ ),
1686
2287
  selectionMenu,
1687
2288
  onSelect: (e) => handleClick(e, annotation),
1688
2289
  style: {
@@ -1724,6 +2325,11 @@ function Annotations(annotationsProps) {
1724
2325
  annotation.object,
1725
2326
  false
1726
2327
  ),
2328
+ isRotatable: resolveInteractionProp(
2329
+ tool == null ? void 0 : tool.interaction.isRotatable,
2330
+ annotation.object,
2331
+ true
2332
+ ),
1727
2333
  selectionMenu,
1728
2334
  onSelect: (e) => handleClick(e, annotation),
1729
2335
  style: {
@@ -1765,6 +2371,11 @@ function Annotations(annotationsProps) {
1765
2371
  annotation.object,
1766
2372
  false
1767
2373
  ),
2374
+ isRotatable: resolveInteractionProp(
2375
+ tool == null ? void 0 : tool.interaction.isRotatable,
2376
+ annotation.object,
2377
+ true
2378
+ ),
1768
2379
  selectionMenu,
1769
2380
  onSelect: (e) => handleClick(e, annotation),
1770
2381
  style: {
@@ -1806,6 +2417,11 @@ function Annotations(annotationsProps) {
1806
2417
  annotation.object,
1807
2418
  false
1808
2419
  ),
2420
+ isRotatable: resolveInteractionProp(
2421
+ tool == null ? void 0 : tool.interaction.isRotatable,
2422
+ annotation.object,
2423
+ false
2424
+ ),
1809
2425
  selectionMenu,
1810
2426
  onSelect: (e) => handleClick(e, annotation),
1811
2427
  zIndex: 0,
@@ -1840,6 +2456,11 @@ function Annotations(annotationsProps) {
1840
2456
  annotation.object,
1841
2457
  false
1842
2458
  ),
2459
+ isRotatable: resolveInteractionProp(
2460
+ tool == null ? void 0 : tool.interaction.isRotatable,
2461
+ annotation.object,
2462
+ false
2463
+ ),
1843
2464
  selectionMenu,
1844
2465
  onSelect: (e) => handleClick(e, annotation),
1845
2466
  zIndex: 0,
@@ -1874,6 +2495,11 @@ function Annotations(annotationsProps) {
1874
2495
  annotation.object,
1875
2496
  false
1876
2497
  ),
2498
+ isRotatable: resolveInteractionProp(
2499
+ tool == null ? void 0 : tool.interaction.isRotatable,
2500
+ annotation.object,
2501
+ false
2502
+ ),
1877
2503
  selectionMenu,
1878
2504
  onSelect: (e) => handleClick(e, annotation),
1879
2505
  zIndex: 0,
@@ -1908,6 +2534,11 @@ function Annotations(annotationsProps) {
1908
2534
  annotation.object,
1909
2535
  false
1910
2536
  ),
2537
+ isRotatable: resolveInteractionProp(
2538
+ tool == null ? void 0 : tool.interaction.isRotatable,
2539
+ annotation.object,
2540
+ false
2541
+ ),
1911
2542
  selectionMenu,
1912
2543
  onSelect: (e) => handleClick(e, annotation),
1913
2544
  zIndex: 0,
@@ -1942,6 +2573,11 @@ function Annotations(annotationsProps) {
1942
2573
  annotation.object,
1943
2574
  false
1944
2575
  ),
2576
+ isRotatable: resolveInteractionProp(
2577
+ tool == null ? void 0 : tool.interaction.isRotatable,
2578
+ annotation.object,
2579
+ true
2580
+ ),
1945
2581
  selectionMenu,
1946
2582
  onSelect: (e) => handleClick(e, annotation),
1947
2583
  vertexConfig: {
@@ -1998,6 +2634,11 @@ function Annotations(annotationsProps) {
1998
2634
  annotation.object,
1999
2635
  false
2000
2636
  ),
2637
+ isRotatable: resolveInteractionProp(
2638
+ tool == null ? void 0 : tool.interaction.isRotatable,
2639
+ annotation.object,
2640
+ true
2641
+ ),
2001
2642
  selectionMenu,
2002
2643
  onSelect: (e) => handleClick(e, annotation),
2003
2644
  vertexConfig: {
@@ -2048,6 +2689,11 @@ function Annotations(annotationsProps) {
2048
2689
  annotation.object,
2049
2690
  false
2050
2691
  ),
2692
+ isRotatable: resolveInteractionProp(
2693
+ tool == null ? void 0 : tool.interaction.isRotatable,
2694
+ annotation.object,
2695
+ true
2696
+ ),
2051
2697
  selectionMenu,
2052
2698
  onSelect: (e) => handleClick(e, annotation),
2053
2699
  vertexConfig: {
@@ -2094,6 +2740,11 @@ function Annotations(annotationsProps) {
2094
2740
  annotation.object,
2095
2741
  false
2096
2742
  ),
2743
+ isRotatable: resolveInteractionProp(
2744
+ tool == null ? void 0 : tool.interaction.isRotatable,
2745
+ annotation.object,
2746
+ true
2747
+ ),
2097
2748
  selectionMenu,
2098
2749
  onSelect: (e) => handleClick(e, annotation),
2099
2750
  style: {
@@ -2144,6 +2795,11 @@ function Annotations(annotationsProps) {
2144
2795
  annotation.object,
2145
2796
  false
2146
2797
  ),
2798
+ isRotatable: resolveInteractionProp(
2799
+ tool == null ? void 0 : tool.interaction.isRotatable,
2800
+ annotation.object,
2801
+ true
2802
+ ),
2147
2803
  selectionMenu,
2148
2804
  onSelect: (e) => handleClick(e, annotation),
2149
2805
  style: {
@@ -2176,6 +2832,7 @@ function Annotations(annotationsProps) {
2176
2832
  isDraggable: false,
2177
2833
  isResizable: false,
2178
2834
  lockAspectRatio: false,
2835
+ isRotatable: false,
2179
2836
  selectionMenu: hasIRT ? void 0 : selectionMenu,
2180
2837
  onSelect: (e) => handleLinkClick(e, annotation),
2181
2838
  ...annotationsProps,
@@ -2207,8 +2864,12 @@ function Annotations(annotationsProps) {
2207
2864
  selectedAnnotations: selectedAnnotationsOnPage,
2208
2865
  isDraggable: areAllSelectedDraggable,
2209
2866
  isResizable: areAllSelectedResizable,
2867
+ isRotatable: areAllSelectedRotatable,
2868
+ lockAspectRatio: shouldLockGroupAspectRatio,
2210
2869
  resizeUI: annotationsProps.resizeUI,
2870
+ rotationUI: annotationsProps.rotationUI,
2211
2871
  selectionOutlineColor: annotationsProps.selectionOutlineColor,
2872
+ selectionOutline: annotationsProps.groupSelectionOutline ?? annotationsProps.selectionOutline,
2212
2873
  groupSelectionMenu: annotationsProps.groupSelectionMenu
2213
2874
  }
2214
2875
  )
@@ -2448,7 +3109,10 @@ function AnnotationLayer({
2448
3109
  groupSelectionMenu,
2449
3110
  resizeUI,
2450
3111
  vertexUI,
3112
+ rotationUI,
2451
3113
  selectionOutlineColor,
3114
+ selectionOutline,
3115
+ groupSelectionOutline,
2452
3116
  customAnnotationRenderer,
2453
3117
  annotationRenderers,
2454
3118
  ...props
@@ -2474,8 +3138,10 @@ function AnnotationLayer({
2474
3138
  }, [overrideScale, documentState == null ? void 0 : documentState.scale]);
2475
3139
  const actualRotation = useMemo(() => {
2476
3140
  if (overrideRotation !== void 0) return overrideRotation;
2477
- return (documentState == null ? void 0 : documentState.rotation) ?? Rotation.Degree0;
2478
- }, [overrideRotation, documentState == null ? void 0 : documentState.rotation]);
3141
+ const pageRotation = (page == null ? void 0 : page.rotation) ?? 0;
3142
+ const docRotation = (documentState == null ? void 0 : documentState.rotation) ?? 0;
3143
+ return (pageRotation + docRotation) % 4;
3144
+ }, [overrideRotation, page == null ? void 0 : page.rotation, documentState == null ? void 0 : documentState.rotation]);
2479
3145
  return /* @__PURE__ */ jsxs(
2480
3146
  "div",
2481
3147
  {
@@ -2497,7 +3163,10 @@ function AnnotationLayer({
2497
3163
  pageHeight: height,
2498
3164
  resizeUI,
2499
3165
  vertexUI,
3166
+ rotationUI,
2500
3167
  selectionOutlineColor,
3168
+ selectionOutline,
3169
+ groupSelectionOutline,
2501
3170
  customAnnotationRenderer,
2502
3171
  annotationRenderers: allRenderers
2503
3172
  }