@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.
- package/dist/preact/index.cjs +1 -1
- package/dist/preact/index.cjs.map +1 -1
- package/dist/preact/index.js +834 -317
- package/dist/preact/index.js.map +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +834 -317
- package/dist/react/index.js.map +1 -1
- package/dist/shared/hooks/use-drag-resize.d.ts +4 -0
- package/dist/shared/hooks/use-interaction-handles.d.ts +18 -2
- package/dist/shared/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
- package/dist/shared/plugin-interaction-primitives/index.d.ts +1 -0
- package/dist/shared/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
- package/dist/shared/plugin-interaction-primitives/utils.d.ts +33 -0
- package/dist/shared-preact/hooks/use-drag-resize.d.ts +4 -0
- package/dist/shared-preact/hooks/use-interaction-handles.d.ts +18 -2
- package/dist/shared-preact/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
- package/dist/shared-preact/plugin-interaction-primitives/index.d.ts +1 -0
- package/dist/shared-preact/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
- package/dist/shared-preact/plugin-interaction-primitives/utils.d.ts +33 -0
- package/dist/shared-react/hooks/use-drag-resize.d.ts +4 -0
- package/dist/shared-react/hooks/use-interaction-handles.d.ts +18 -2
- package/dist/shared-react/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
- package/dist/shared-react/plugin-interaction-primitives/index.d.ts +1 -0
- package/dist/shared-react/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
- package/dist/shared-react/plugin-interaction-primitives/utils.d.ts +33 -0
- package/dist/shared-svelte/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
- package/dist/shared-svelte/plugin-interaction-primitives/index.d.ts +1 -0
- package/dist/shared-svelte/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
- package/dist/shared-svelte/plugin-interaction-primitives/utils.d.ts +33 -0
- package/dist/shared-vue/plugin-interaction-primitives/drag-resize-controller.d.ts +49 -23
- package/dist/shared-vue/plugin-interaction-primitives/index.d.ts +1 -0
- package/dist/shared-vue/plugin-interaction-primitives/resize-geometry.d.ts +72 -0
- package/dist/shared-vue/plugin-interaction-primitives/utils.d.ts +33 -0
- package/dist/svelte/hooks/use-drag-resize.svelte.d.ts +1 -0
- package/dist/svelte/hooks/use-interaction-handles.svelte.d.ts +16 -2
- package/dist/svelte/index.cjs +1 -1
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.js +680 -288
- package/dist/svelte/index.js.map +1 -1
- package/dist/vue/hooks/use-drag-resize.d.ts +9 -0
- package/dist/vue/hooks/use-interaction-handles.d.ts +17 -2
- package/dist/vue/index.cjs +1 -1
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.js +716 -292
- package/dist/vue/index.js.map +1 -1
- package/package.json +2 -2
package/dist/svelte/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
516
|
+
rotationAngle: initialRotation,
|
|
517
|
+
rotationDelta: 0,
|
|
518
|
+
rotationCenter: this.rotationCenter,
|
|
519
|
+
isSnapped: false
|
|
79
520
|
}
|
|
80
521
|
}
|
|
81
522
|
});
|
|
82
523
|
}
|
|
83
|
-
|
|
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.
|
|
100
|
-
const position =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
832
|
+
* Calculate the angle from the center to a point in screen coordinates.
|
|
342
833
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
}
|