kritzel-stencil 0.0.161 → 0.0.162

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 (126) hide show
  1. package/dist/cjs/{default-text-tool.config-zB3FPuXq.js → default-line-tool.config-D1Ns0NmM.js} +3156 -1052
  2. package/dist/cjs/default-line-tool.config-D1Ns0NmM.js.map +1 -0
  3. package/dist/cjs/index.cjs.js +131 -127
  4. package/dist/cjs/index.cjs.js.map +1 -1
  5. package/dist/cjs/kritzel-color_22.cjs.entry.js +480 -147
  6. package/dist/collection/classes/core/core.class.js +140 -3
  7. package/dist/collection/classes/core/core.class.js.map +1 -1
  8. package/dist/collection/classes/core/reviver.class.js +8 -0
  9. package/dist/collection/classes/core/reviver.class.js.map +1 -1
  10. package/dist/collection/classes/core/store.class.js +5 -0
  11. package/dist/collection/classes/core/store.class.js.map +1 -1
  12. package/dist/collection/classes/handlers/line-handle.handler.js +383 -0
  13. package/dist/collection/classes/handlers/line-handle.handler.js.map +1 -0
  14. package/dist/collection/classes/handlers/move.handler.js +2 -2
  15. package/dist/collection/classes/handlers/move.handler.js.map +1 -1
  16. package/dist/collection/classes/managers/anchor.manager.js +874 -0
  17. package/dist/collection/classes/managers/anchor.manager.js.map +1 -0
  18. package/dist/collection/classes/managers/cursor.manager.js +117 -0
  19. package/dist/collection/classes/managers/cursor.manager.js.map +1 -0
  20. package/dist/collection/classes/objects/base-object.class.js +4 -2
  21. package/dist/collection/classes/objects/base-object.class.js.map +1 -1
  22. package/dist/collection/classes/objects/line.class.js +564 -0
  23. package/dist/collection/classes/objects/line.class.js.map +1 -0
  24. package/dist/collection/classes/objects/selection-group.class.js +4 -0
  25. package/dist/collection/classes/objects/selection-group.class.js.map +1 -1
  26. package/dist/collection/classes/registries/icon-registry.class.js +1 -0
  27. package/dist/collection/classes/registries/icon-registry.class.js.map +1 -1
  28. package/dist/collection/classes/tools/line-tool.class.js +172 -0
  29. package/dist/collection/classes/tools/line-tool.class.js.map +1 -0
  30. package/dist/collection/classes/tools/selection-tool.class.js +41 -8
  31. package/dist/collection/classes/tools/selection-tool.class.js.map +1 -1
  32. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +11 -2
  33. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js.map +1 -1
  34. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +130 -58
  35. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js.map +1 -1
  36. package/dist/collection/configs/default-engine-config.js +4 -0
  37. package/dist/collection/configs/default-engine-config.js.map +1 -1
  38. package/dist/collection/configs/default-line-tool.config.js +34 -0
  39. package/dist/collection/configs/default-line-tool.config.js.map +1 -0
  40. package/dist/collection/helpers/geometry.helper.js +42 -0
  41. package/dist/collection/helpers/geometry.helper.js.map +1 -1
  42. package/dist/collection/index.js +5 -0
  43. package/dist/collection/index.js.map +1 -1
  44. package/dist/collection/interfaces/anchor.interface.js +2 -0
  45. package/dist/collection/interfaces/anchor.interface.js.map +1 -0
  46. package/dist/collection/interfaces/arrow-head.interface.js +2 -0
  47. package/dist/collection/interfaces/arrow-head.interface.js.map +1 -0
  48. package/dist/collection/interfaces/engine-state.interface.js.map +1 -1
  49. package/dist/collection/interfaces/line-options.interface.js +2 -0
  50. package/dist/collection/interfaces/line-options.interface.js.map +1 -0
  51. package/dist/collection/interfaces/toolbar-control.interface.js.map +1 -1
  52. package/dist/components/index.js +4 -4
  53. package/dist/components/kritzel-brush-style.js +1 -1
  54. package/dist/components/kritzel-context-menu.js +1 -1
  55. package/dist/components/kritzel-control-brush-config.js +1 -1
  56. package/dist/components/kritzel-control-text-config.js +1 -1
  57. package/dist/components/kritzel-controls.js +1 -1
  58. package/dist/components/kritzel-editor.js +54 -13
  59. package/dist/components/kritzel-editor.js.map +1 -1
  60. package/dist/components/kritzel-engine.js +1 -1
  61. package/dist/components/kritzel-icon.js +1 -1
  62. package/dist/components/kritzel-menu-item.js +1 -1
  63. package/dist/components/kritzel-menu.js +1 -1
  64. package/dist/components/kritzel-split-button.js +1 -1
  65. package/dist/components/kritzel-utility-panel.js +1 -1
  66. package/dist/components/kritzel-workspace-manager.js +1 -1
  67. package/dist/components/{p-BdZKPKnx.js → p-7_lwv0zQ.js} +4 -4
  68. package/dist/components/{p-BdZKPKnx.js.map → p-7_lwv0zQ.js.map} +1 -1
  69. package/dist/components/{p-DbKKCHKd.js → p-BixlbUD7.js} +3 -2
  70. package/dist/components/p-BixlbUD7.js.map +1 -0
  71. package/dist/components/{p-Doixm8-N.js → p-CDteBYm9.js} +3 -3
  72. package/dist/components/{p-Doixm8-N.js.map → p-CDteBYm9.js.map} +1 -1
  73. package/dist/components/{p-58y59Acb.js → p-CkD1PQQX.js} +5 -5
  74. package/dist/components/{p-58y59Acb.js.map → p-CkD1PQQX.js.map} +1 -1
  75. package/dist/components/{p-DxNbcUzt.js → p-Cqr0Bah5.js} +3 -3
  76. package/dist/components/{p-DxNbcUzt.js.map → p-Cqr0Bah5.js.map} +1 -1
  77. package/dist/components/{p-D7BLVRXX.js → p-CuhOrcET.js} +2726 -380
  78. package/dist/components/p-CuhOrcET.js.map +1 -0
  79. package/dist/components/{p-i0IlGLv2.js → p-CvLFRlQU.js} +3 -3
  80. package/dist/components/{p-i0IlGLv2.js.map → p-CvLFRlQU.js.map} +1 -1
  81. package/dist/components/{p-BpXgwgnV.js → p-DKwJJuFb.js} +7 -7
  82. package/dist/components/{p-BpXgwgnV.js.map → p-DKwJJuFb.js.map} +1 -1
  83. package/dist/components/{p-CC8KFHSe.js → p-DZ7kxJUx.js} +3 -3
  84. package/dist/components/{p-CC8KFHSe.js.map → p-DZ7kxJUx.js.map} +1 -1
  85. package/dist/components/{p-D_ygcWSz.js → p-dMCB4tJA.js} +3 -3
  86. package/dist/components/{p-D_ygcWSz.js.map → p-dMCB4tJA.js.map} +1 -1
  87. package/dist/components/{p-CBYBurdY.js → p-sokRZ7Vn.js} +49 -5
  88. package/dist/components/p-sokRZ7Vn.js.map +1 -0
  89. package/dist/esm/{default-text-tool.config-BvCgOiKA.js → default-line-tool.config-C35m-d1Y.js} +3152 -1053
  90. package/dist/esm/default-line-tool.config-C35m-d1Y.js.map +1 -0
  91. package/dist/esm/index.js +2 -2
  92. package/dist/esm/kritzel-color_22.entry.js +399 -66
  93. package/dist/stencil/index.esm.js +1 -1
  94. package/dist/stencil/p-C35m-d1Y.js +2 -0
  95. package/dist/stencil/p-C35m-d1Y.js.map +1 -0
  96. package/dist/stencil/p-d142ef46.entry.js +10 -0
  97. package/dist/stencil/p-d142ef46.entry.js.map +1 -0
  98. package/dist/stencil/stencil.esm.js +1 -1
  99. package/dist/types/classes/core/core.class.d.ts +18 -0
  100. package/dist/types/classes/core/store.class.d.ts +2 -0
  101. package/dist/types/classes/handlers/line-handle.handler.d.ts +34 -0
  102. package/dist/types/classes/managers/anchor.manager.d.ts +160 -0
  103. package/dist/types/classes/managers/cursor.manager.d.ts +43 -0
  104. package/dist/types/classes/objects/line.class.d.ts +98 -0
  105. package/dist/types/classes/tools/line-tool.class.d.ts +17 -0
  106. package/dist/types/classes/tools/selection-tool.class.d.ts +4 -0
  107. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +2 -4
  108. package/dist/types/components.d.ts +5 -5
  109. package/dist/types/configs/default-line-tool.config.d.ts +2 -0
  110. package/dist/types/helpers/geometry.helper.d.ts +10 -0
  111. package/dist/types/index.d.ts +5 -0
  112. package/dist/types/interfaces/anchor.interface.d.ts +137 -0
  113. package/dist/types/interfaces/arrow-head.interface.d.ts +26 -0
  114. package/dist/types/interfaces/engine-state.interface.d.ts +8 -0
  115. package/dist/types/interfaces/line-options.interface.d.ts +21 -0
  116. package/dist/types/interfaces/toolbar-control.interface.d.ts +17 -1
  117. package/package.json +1 -1
  118. package/dist/cjs/default-text-tool.config-zB3FPuXq.js.map +0 -1
  119. package/dist/components/p-CBYBurdY.js.map +0 -1
  120. package/dist/components/p-D7BLVRXX.js.map +0 -1
  121. package/dist/components/p-DbKKCHKd.js.map +0 -1
  122. package/dist/esm/default-text-tool.config-BvCgOiKA.js.map +0 -1
  123. package/dist/stencil/p-6d9756d9.entry.js +0 -10
  124. package/dist/stencil/p-6d9756d9.entry.js.map +0 -1
  125. package/dist/stencil/p-BvCgOiKA.js +0 -2
  126. package/dist/stencil/p-BvCgOiKA.js.map +0 -1
@@ -0,0 +1,874 @@
1
+ import { KritzelLine } from "../objects/line.class";
2
+ import { KritzelSelectionBox } from "../objects/selection-box.class";
3
+ import { KritzelSelectionGroup } from "../objects/selection-group.class";
4
+ import { KritzelGeometryHelper } from "../../helpers/geometry.helper";
5
+ import { KritzelClassHelper } from "../../helpers/class.helper";
6
+ /**
7
+ * Manages anchor connections between line endpoints and other objects.
8
+ * Maintains a runtime index for efficient reverse lookups and handles
9
+ * snap detection during line endpoint dragging.
10
+ */
11
+ export class KritzelAnchorManager {
12
+ _core;
13
+ /**
14
+ * Runtime index mapping objectId to the lines anchored to it.
15
+ * This is derived from the anchor properties on lines and rebuilt as needed.
16
+ */
17
+ _anchorIndex = new Map();
18
+ constructor(core) {
19
+ this._core = core;
20
+ }
21
+ // ============================================
22
+ // Anchor CRUD Operations
23
+ // ============================================
24
+ /**
25
+ * Sets an anchor from a line endpoint to a target object's center.
26
+ * Updates both the line's anchor property and the internal index.
27
+ */
28
+ setAnchor(lineId, endpoint, targetObjectId) {
29
+ const line = this.getLineById(lineId);
30
+ if (!line) {
31
+ return;
32
+ }
33
+ // Prevent anchoring both endpoints to the same object
34
+ if (endpoint === 'start' && line.endAnchor?.objectId === targetObjectId) {
35
+ return;
36
+ }
37
+ if (endpoint === 'end' && line.startAnchor?.objectId === targetObjectId) {
38
+ return;
39
+ }
40
+ // Remove existing anchor if any
41
+ this.removeAnchor(lineId, endpoint);
42
+ // Set the anchor on the line
43
+ const anchor = { objectId: targetObjectId };
44
+ if (endpoint === 'start') {
45
+ line.startAnchor = anchor;
46
+ }
47
+ else {
48
+ line.endAnchor = anchor;
49
+ }
50
+ // Update the index
51
+ this.addToIndex(targetObjectId, lineId, endpoint);
52
+ // Snap the endpoint to the target's center
53
+ this.snapEndpointToObject(line, endpoint, targetObjectId);
54
+ // Persist the change
55
+ this._core.store.state.objects.update(line);
56
+ }
57
+ /**
58
+ * Removes an anchor from a line endpoint.
59
+ * Updates both the line's anchor property and the internal index.
60
+ */
61
+ removeAnchor(lineId, endpoint) {
62
+ const line = this.getLineById(lineId);
63
+ if (!line) {
64
+ return;
65
+ }
66
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
67
+ if (!anchor) {
68
+ return;
69
+ }
70
+ // Remove from index
71
+ this.removeFromIndex(anchor.objectId, lineId, endpoint);
72
+ // Clear the anchor on the line
73
+ if (endpoint === 'start') {
74
+ line.startAnchor = undefined;
75
+ }
76
+ else {
77
+ line.endAnchor = undefined;
78
+ }
79
+ }
80
+ /**
81
+ * Gets the anchor for a specific line endpoint.
82
+ */
83
+ getAnchor(lineId, endpoint) {
84
+ const line = this.getLineById(lineId);
85
+ if (!line) {
86
+ return null;
87
+ }
88
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
89
+ return anchor ?? null;
90
+ }
91
+ // ============================================
92
+ // Query Operations
93
+ // ============================================
94
+ /**
95
+ * Gets all lines that have an endpoint anchored to the specified object.
96
+ */
97
+ getLinesAnchoredTo(objectId) {
98
+ const entries = this._anchorIndex.get(objectId);
99
+ return entries ? Array.from(entries) : [];
100
+ }
101
+ // ============================================
102
+ // Position Updates
103
+ // ============================================
104
+ /**
105
+ * Updates all line endpoints that are anchored to the specified object.
106
+ * Should be called when an object moves.
107
+ */
108
+ updateAnchorsForObject(objectId) {
109
+ const entries = this.getLinesAnchoredTo(objectId);
110
+ if (entries.length === 0) {
111
+ return;
112
+ }
113
+ const targetObject = this.getObjectById(objectId);
114
+ if (!targetObject) {
115
+ return;
116
+ }
117
+ for (const entry of entries) {
118
+ const line = this.getLineById(entry.lineId);
119
+ if (!line) {
120
+ continue;
121
+ }
122
+ this.snapEndpointToObject(line, entry.endpoint, objectId);
123
+ }
124
+ }
125
+ /**
126
+ * Snaps a line endpoint to an object's center.
127
+ */
128
+ snapEndpointToObject(line, endpoint, targetObjectId) {
129
+ const targetObject = this.getObjectById(targetObjectId);
130
+ if (!targetObject) {
131
+ return;
132
+ }
133
+ // Get target center in world coordinates
134
+ const targetCenterX = targetObject.centerX;
135
+ const targetCenterY = targetObject.centerY;
136
+ // Convert world coordinates to line's local coordinate system
137
+ const localCoords = this.worldToLineLocal(line, targetCenterX, targetCenterY);
138
+ // Update the endpoint
139
+ line.updateEndpoint(endpoint, localCoords.x, localCoords.y);
140
+ }
141
+ /**
142
+ * Converts world coordinates to a line's local coordinate system.
143
+ */
144
+ worldToLineLocal(line, worldX, worldY) {
145
+ // Get line center in world coordinates
146
+ const cx = line.centerX;
147
+ const cy = line.centerY;
148
+ // Translate to center
149
+ const dx = worldX - cx;
150
+ const dy = worldY - cy;
151
+ // Apply inverse rotation
152
+ const cos = Math.cos(-line.rotation);
153
+ const sin = Math.sin(-line.rotation);
154
+ const rotatedX = dx * cos - dy * sin;
155
+ const rotatedY = dx * sin + dy * cos;
156
+ // Calculate local coordinates relative to the unrotated bounding box top-left
157
+ const relativeX = rotatedX + (line.totalWidth / 2) / line.scale;
158
+ const relativeY = rotatedY + (line.totalHeight / 2) / line.scale;
159
+ // Convert to internal coordinates
160
+ const localX = relativeX * line.scale + line.x;
161
+ const localY = relativeY * line.scale + line.y;
162
+ return { x: localX, y: localY };
163
+ }
164
+ // ============================================
165
+ // Snap Detection
166
+ // ============================================
167
+ /**
168
+ * Finds the nearest object that can be snapped to within the snap threshold.
169
+ * Returns null if no suitable snap target is found.
170
+ *
171
+ * @param worldX - X coordinate in world space
172
+ * @param worldY - Y coordinate in world space
173
+ * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
174
+ * @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
175
+ */
176
+ findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
177
+ let bestTarget = null;
178
+ let highestZIndex = -Infinity;
179
+ const objects = this._core.store.allNonSelectionObjects;
180
+ for (const object of objects) {
181
+ // Skip the line being edited
182
+ if (object.id === excludeLineId) {
183
+ continue;
184
+ }
185
+ // Skip if this is the object anchored to the other endpoint
186
+ if (otherEndpointAnchorId && object.id === otherEndpointAnchorId) {
187
+ continue;
188
+ }
189
+ // Skip non-anchorable objects
190
+ if (!this.isAnchorable(object)) {
191
+ continue;
192
+ }
193
+ // Check if point is inside the object's rotated polygon
194
+ const polygon = object.rotatedPolygon;
195
+ const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
196
+ if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
197
+ // If inside, check if this object is "above" the current best target
198
+ if (object.zIndex > highestZIndex) {
199
+ highestZIndex = object.zIndex;
200
+ bestTarget = {
201
+ objectId: object.id,
202
+ centerX: object.centerX,
203
+ centerY: object.centerY,
204
+ };
205
+ }
206
+ }
207
+ }
208
+ return bestTarget;
209
+ }
210
+ /**
211
+ * Sets the current snap candidate for visual feedback.
212
+ */
213
+ setSnapCandidate(candidate) {
214
+ this._core.store.state.snapCandidate = candidate;
215
+ this._core.rerender();
216
+ }
217
+ /**
218
+ * Gets the current snap candidate.
219
+ */
220
+ getSnapCandidate() {
221
+ return this._core.store.state.snapCandidate ?? null;
222
+ }
223
+ /**
224
+ * Clears the snap candidate.
225
+ */
226
+ clearSnapCandidate() {
227
+ this._core.store.state.snapCandidate = null;
228
+ this._core.rerender();
229
+ }
230
+ // ============================================
231
+ // Render Data
232
+ // ============================================
233
+ /**
234
+ * Gets render data for anchor lines visualization.
235
+ * Returns null if there's no selected line with anchors or if snap is in progress.
236
+ */
237
+ getAnchorLinesRenderData() {
238
+ const selectionGroup = this._core.store.selectionGroup;
239
+ if (!selectionGroup || selectionGroup.objects.length !== 1)
240
+ return null;
241
+ const selectedObject = selectionGroup.objects[0];
242
+ if (!KritzelClassHelper.isInstanceOf(selectedObject, 'KritzelLine'))
243
+ return null;
244
+ const line = selectedObject;
245
+ const startAnchorViz = this.computeAnchorVisualization(line, 'start');
246
+ const endAnchorViz = this.computeAnchorVisualization(line, 'end');
247
+ if (!startAnchorViz && !endAnchorViz)
248
+ return null;
249
+ const scale = this._core.store.state.scale;
250
+ const lineStrokeWidth = line.strokeWidth / line.scale;
251
+ const indicatorStrokeWidth = `${2 / scale}`;
252
+ const dashLength = Math.max(lineStrokeWidth * 2, 4 / scale);
253
+ const dashArray = `${dashLength} ${dashLength}`;
254
+ const indicatorRadius = 8 / scale;
255
+ return {
256
+ lineStrokeWidth,
257
+ indicatorStrokeWidth,
258
+ dashArray,
259
+ indicatorRadius,
260
+ startAnchorViz,
261
+ endAnchorViz,
262
+ };
263
+ }
264
+ /**
265
+ * Gets render data for snap indicator visualization.
266
+ * Returns null if there's no snap candidate.
267
+ */
268
+ getSnapIndicatorRenderData() {
269
+ const snapCandidate = this.getSnapCandidate();
270
+ if (!snapCandidate)
271
+ return null;
272
+ const scale = this._core.store.state.scale;
273
+ const indicatorRadius = 8 / scale;
274
+ const indicatorStrokeWidth = `${2 / scale}`;
275
+ const lineStrokeWidth = snapCandidate.lineStrokeWidth
276
+ ? `${snapCandidate.lineStrokeWidth}`
277
+ : `${4 / scale}`;
278
+ const lineStrokeWidthNum = snapCandidate.lineStrokeWidth || (4 / scale);
279
+ const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
280
+ const dashArray = `${dashLength} ${dashLength}`;
281
+ const lineStroke = snapCandidate.lineStroke || '#000000';
282
+ let solidLineEndX = snapCandidate.edgeX;
283
+ let solidLineEndY = snapCandidate.edgeY;
284
+ let arrowPoints;
285
+ if (snapCandidate.arrowOffset && snapCandidate.edgeX !== undefined && snapCandidate.edgeY !== undefined) {
286
+ const dx = snapCandidate.lineEndpointX - snapCandidate.edgeX;
287
+ const dy = snapCandidate.lineEndpointY - snapCandidate.edgeY;
288
+ const length = Math.sqrt(dx * dx + dy * dy);
289
+ if (length > snapCandidate.arrowOffset) {
290
+ solidLineEndX = snapCandidate.edgeX + (dx / length) * snapCandidate.arrowOffset;
291
+ solidLineEndY = snapCandidate.edgeY + (dy / length) * snapCandidate.arrowOffset;
292
+ }
293
+ // Calculate arrow head points
294
+ // Direction from line endpoint to edge (arrow direction)
295
+ const arrowDx = snapCandidate.edgeX - snapCandidate.lineEndpointX;
296
+ const arrowDy = snapCandidate.edgeY - snapCandidate.lineEndpointY;
297
+ const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
298
+ if (arrowLengthTotal > 0) {
299
+ const ux = arrowDx / arrowLengthTotal;
300
+ const uy = arrowDy / arrowLengthTotal;
301
+ // Perpendicular vector
302
+ const px = -uy;
303
+ const py = ux;
304
+ // Arrow dimensions
305
+ const arrowLength = snapCandidate.arrowOffset;
306
+ const arrowWidth = arrowLength; // 1:1 ratio
307
+ // Arrow tip at edge
308
+ const tipX = snapCandidate.edgeX;
309
+ const tipY = snapCandidate.edgeY;
310
+ // Arrow base
311
+ const baseX = tipX - ux * arrowLength;
312
+ const baseY = tipY - uy * arrowLength;
313
+ // Arrow wings
314
+ const leftX = baseX + px * arrowWidth / 2;
315
+ const leftY = baseY + py * arrowWidth / 2;
316
+ const rightX = baseX - px * arrowWidth / 2;
317
+ const rightY = baseY - py * arrowWidth / 2;
318
+ arrowPoints = `${tipX},${tipY} ${leftX},${leftY} ${rightX},${rightY}`;
319
+ }
320
+ }
321
+ return {
322
+ indicatorRadius,
323
+ indicatorStrokeWidth,
324
+ lineStrokeWidth,
325
+ dashArray,
326
+ lineStroke,
327
+ centerX: snapCandidate.centerX,
328
+ centerY: snapCandidate.centerY,
329
+ lineEndpointX: snapCandidate.lineEndpointX,
330
+ lineEndpointY: snapCandidate.lineEndpointY,
331
+ edgeX: snapCandidate.edgeX,
332
+ edgeY: snapCandidate.edgeY,
333
+ arrowOffset: snapCandidate.arrowOffset,
334
+ arrowStyle: snapCandidate.arrowStyle,
335
+ arrowFill: snapCandidate.arrowFill,
336
+ solidLineEndX,
337
+ solidLineEndY,
338
+ arrowPoints,
339
+ snapLinePath: (() => {
340
+ if (snapCandidate.controlX !== undefined &&
341
+ snapCandidate.controlY !== undefined &&
342
+ snapCandidate.t !== undefined) {
343
+ const startT = snapCandidate.endpoint === 'start' ? 1 - snapCandidate.t : snapCandidate.t;
344
+ // Ensure meaningful range
345
+ if (startT >= 1)
346
+ return undefined;
347
+ const segment = this.extractQuadraticSegment({ x: snapCandidate.lineEndpointX, y: snapCandidate.lineEndpointY }, { x: snapCandidate.controlX, y: snapCandidate.controlY }, { x: snapCandidate.centerX, y: snapCandidate.centerY }, startT, 1);
348
+ return `M ${segment.start.x} ${segment.start.y} Q ${segment.control.x} ${segment.control.y} ${segment.end.x} ${segment.end.y}`;
349
+ }
350
+ return undefined;
351
+ })(),
352
+ };
353
+ }
354
+ // ============================================
355
+ // Cleanup Operations
356
+ // ============================================
357
+ /**
358
+ * Handles cleanup when an object is deleted.
359
+ * Detaches all lines that were anchored to the deleted object.
360
+ */
361
+ handleObjectDeleted(objectId) {
362
+ const entries = this.getLinesAnchoredTo(objectId);
363
+ for (const entry of entries) {
364
+ this.removeAnchor(entry.lineId, entry.endpoint);
365
+ // Update the line to persist the change
366
+ const line = this.getLineById(entry.lineId);
367
+ if (line) {
368
+ this._core.store.state.objects.update(line);
369
+ }
370
+ }
371
+ // Remove the object from the index
372
+ this._anchorIndex.delete(objectId);
373
+ }
374
+ /**
375
+ * Handles cleanup when a line is deleted.
376
+ * Removes all anchor index entries for the line.
377
+ */
378
+ handleLineDeleted(lineId) {
379
+ const line = this.getLineById(lineId);
380
+ if (!line) {
381
+ return;
382
+ }
383
+ if (line.startAnchor) {
384
+ this.removeFromIndex(line.startAnchor.objectId, lineId, 'start');
385
+ }
386
+ if (line.endAnchor) {
387
+ this.removeFromIndex(line.endAnchor.objectId, lineId, 'end');
388
+ }
389
+ }
390
+ // ============================================
391
+ // Index Management
392
+ // ============================================
393
+ /**
394
+ * Rebuilds the anchor index from all lines in the object map.
395
+ * Should be called after loading/deserializing objects.
396
+ */
397
+ rebuildIndex() {
398
+ this._anchorIndex.clear();
399
+ const objects = this._core.store.allObjects;
400
+ for (const object of objects) {
401
+ if (object instanceof KritzelLine) {
402
+ if (object.startAnchor) {
403
+ this.addToIndex(object.startAnchor.objectId, object.id, 'start');
404
+ }
405
+ if (object.endAnchor) {
406
+ this.addToIndex(object.endAnchor.objectId, object.id, 'end');
407
+ }
408
+ }
409
+ }
410
+ }
411
+ /**
412
+ * Adds an entry to the anchor index.
413
+ */
414
+ addToIndex(objectId, lineId, endpoint) {
415
+ if (!this._anchorIndex.has(objectId)) {
416
+ this._anchorIndex.set(objectId, new Set());
417
+ }
418
+ const entries = this._anchorIndex.get(objectId);
419
+ // Check if entry already exists
420
+ for (const entry of entries) {
421
+ if (entry.lineId === lineId && entry.endpoint === endpoint) {
422
+ return;
423
+ }
424
+ }
425
+ entries.add({ lineId, endpoint });
426
+ }
427
+ /**
428
+ * Removes an entry from the anchor index.
429
+ */
430
+ removeFromIndex(objectId, lineId, endpoint) {
431
+ const entries = this._anchorIndex.get(objectId);
432
+ if (!entries) {
433
+ return;
434
+ }
435
+ for (const entry of entries) {
436
+ if (entry.lineId === lineId && entry.endpoint === endpoint) {
437
+ entries.delete(entry);
438
+ break;
439
+ }
440
+ }
441
+ // Clean up empty sets
442
+ if (entries.size === 0) {
443
+ this._anchorIndex.delete(objectId);
444
+ }
445
+ }
446
+ // ============================================
447
+ // Helper Methods
448
+ // ============================================
449
+ /**
450
+ * Gets a line by its ID.
451
+ */
452
+ getLineById(lineId) {
453
+ const objects = this._core.store.state.objects.filter(o => o.id === lineId);
454
+ if (objects.length === 0) {
455
+ return null;
456
+ }
457
+ const object = objects[0];
458
+ return object instanceof KritzelLine ? object : null;
459
+ }
460
+ /**
461
+ * Gets an object by its ID.
462
+ */
463
+ getObjectById(objectId) {
464
+ const objects = this._core.store.state.objects.filter(o => o.id === objectId);
465
+ return objects.length > 0 ? objects[0] : null;
466
+ }
467
+ findAnchorTarget(line, endpoint) {
468
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
469
+ if (!anchor) {
470
+ return null;
471
+ }
472
+ return this._core.store.allNonSelectionObjects.find(obj => obj.id === anchor.objectId) ?? null;
473
+ }
474
+ // ============================================
475
+ // Visualization Helpers
476
+ // ============================================
477
+ /**
478
+ * Computes anchor visualization data for a line with anchors.
479
+ * Returns edge intersection points for rendering dashed lines from edge to center.
480
+ */
481
+ computeAnchorVisualization(line, endpoint) {
482
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
483
+ if (!anchor)
484
+ return null;
485
+ const targetObject = this.findAnchorTarget(line, endpoint);
486
+ if (!targetObject)
487
+ return null;
488
+ const clipInfo = this.computeAnchorClipInfo(line, endpoint, targetObject);
489
+ if (!clipInfo)
490
+ return null;
491
+ const centerX = targetObject.centerX;
492
+ const centerY = targetObject.centerY;
493
+ return {
494
+ edgeX: clipInfo.worldX,
495
+ edgeY: clipInfo.worldY,
496
+ centerX,
497
+ centerY,
498
+ pathD: this.buildAnchorPath(line, endpoint, clipInfo, targetObject) ?? undefined,
499
+ };
500
+ }
501
+ /**
502
+ * Computes a clipped line path that stops at anchor edges instead of going to anchor centers.
503
+ * When arrows are present on anchored ends, the line is shortened so the arrow tip appears at the edge.
504
+ * Returns the SVG path 'd' attribute string, or the original path if no clipping is needed.
505
+ * @param line The line object
506
+ * @param offsetByViewBox If true, subtracts line.x and line.y from coordinates (for selection UI)
507
+ */
508
+ computeClippedLinePath(line, offsetByViewBox = false) {
509
+ // Check for snap candidate first - this applies during drag operations even without anchors
510
+ const snapCandidate = this.getSnapCandidate();
511
+ const selectionGroup = this._core.store.selectionGroup;
512
+ const hasActiveSnapCandidate = snapCandidate && selectionGroup && selectionGroup.objects.length === 1 && selectionGroup.objects[0].id === line.id;
513
+ // If no anchors and no active snap candidate, return original path (with or without offset)
514
+ if (!line.startAnchor && !line.endAnchor && !hasActiveSnapCandidate) {
515
+ if (offsetByViewBox) {
516
+ if (line.controlX !== undefined && line.controlY !== undefined) {
517
+ return `M ${line.startX - line.x} ${line.startY - line.y} Q ${line.controlX - line.x} ${line.controlY - line.y} ${line.endX - line.x} ${line.endY - line.y}`;
518
+ }
519
+ return `M ${line.startX - line.x} ${line.startY - line.y} L ${line.endX - line.x} ${line.endY - line.y}`;
520
+ }
521
+ return line.d;
522
+ }
523
+ const startTarget = line.startAnchor ? this.findAnchorTarget(line, 'start') : null;
524
+ const endTarget = line.endAnchor ? this.findAnchorTarget(line, 'end') : null;
525
+ let startClip = startTarget ? this.computeAnchorClipInfo(line, 'start', startTarget) : null;
526
+ let endClip = endTarget ? this.computeAnchorClipInfo(line, 'end', endTarget) : null;
527
+ // Apply snap candidate if present and matches this line
528
+ if (hasActiveSnapCandidate) {
529
+ if (snapCandidate.edgeX !== undefined && snapCandidate.edgeY !== undefined) {
530
+ const localEdge = this.lineWorldToLocal(line, snapCandidate.edgeX, snapCandidate.edgeY);
531
+ const clipInfo = {
532
+ worldX: snapCandidate.edgeX,
533
+ worldY: snapCandidate.edgeY,
534
+ localX: localEdge.x,
535
+ localY: localEdge.y,
536
+ t: snapCandidate.t
537
+ };
538
+ if (snapCandidate.endpoint === 'start') {
539
+ startClip = clipInfo;
540
+ }
541
+ else {
542
+ endClip = clipInfo;
543
+ }
544
+ }
545
+ }
546
+ // Apply offset if requested (for selection UI which uses coordinates relative to 0,0)
547
+ const offsetX = offsetByViewBox ? line.x : 0;
548
+ const offsetY = offsetByViewBox ? line.y : 0;
549
+ // Handle curved lines by splitting the quadratic at the clipped parameters
550
+ if (line.controlX !== undefined && line.controlY !== undefined) {
551
+ let startT = startClip?.t ?? 0;
552
+ let endT = endClip?.t ?? 1;
553
+ // Adjust for start arrow
554
+ if (startClip && line.hasStartArrow) {
555
+ const arrowOffset = line.getArrowSize('start');
556
+ const speed = this.evaluateDerivativeSpeedAtT(line, startT);
557
+ if (speed > 0) {
558
+ startT += arrowOffset / speed;
559
+ }
560
+ }
561
+ // Adjust for end arrow
562
+ if (endClip && line.hasEndArrow) {
563
+ const arrowOffset = line.getArrowSize('end');
564
+ const speed = this.evaluateDerivativeSpeedAtT(line, endT);
565
+ if (speed > 0) {
566
+ endT -= arrowOffset / speed;
567
+ }
568
+ }
569
+ // Ensure valid range
570
+ if (startT < 0)
571
+ startT = 0;
572
+ if (endT > 1)
573
+ endT = 1;
574
+ // Handle overlap or invalid range
575
+ if (endT <= startT) {
576
+ // If the arrow adjustment caused them to cross, or they were already crossed/close
577
+ // We can either effectively hide the line, or just clamp them.
578
+ // If we clamp them to the midpoint, the line becomes a point.
579
+ // Let's ensure a minimal gap or just set them equal.
580
+ const mid = (startT + endT) / 2;
581
+ startT = mid;
582
+ endT = mid;
583
+ }
584
+ const segment = this.extractQuadraticSegment({ x: line.startX, y: line.startY }, { x: line.controlX, y: line.controlY }, { x: line.endX, y: line.endY }, startT, endT);
585
+ return `M ${segment.start.x - offsetX} ${segment.start.y - offsetY} Q ${segment.control.x - offsetX} ${segment.control.y - offsetY} ${segment.end.x - offsetX} ${segment.end.y - offsetY}`;
586
+ }
587
+ // Straight lines fall back to linear interpolation
588
+ let startX = startClip?.localX ?? line.startX;
589
+ let startY = startClip?.localY ?? line.startY;
590
+ let endX = endClip?.localX ?? line.endX;
591
+ let endY = endClip?.localY ?? line.endY;
592
+ if (startClip && line.hasStartArrow) {
593
+ const arrowOffset = line.getArrowSize('start');
594
+ const dx = endX - startX;
595
+ const dy = endY - startY;
596
+ const length = Math.sqrt(dx * dx + dy * dy);
597
+ if (length > arrowOffset) {
598
+ startX += (dx / length) * arrowOffset;
599
+ startY += (dy / length) * arrowOffset;
600
+ }
601
+ }
602
+ if (endClip && line.hasEndArrow) {
603
+ const arrowOffset = line.getArrowSize('end');
604
+ const dx = startX - endX;
605
+ const dy = startY - endY;
606
+ const length = Math.sqrt(dx * dx + dy * dy);
607
+ if (length > arrowOffset) {
608
+ endX += (dx / length) * arrowOffset;
609
+ endY += (dy / length) * arrowOffset;
610
+ }
611
+ }
612
+ return `M ${startX - offsetX} ${startY - offsetY} L ${endX - offsetX} ${endY - offsetY}`;
613
+ }
614
+ computeAnchorClipInfo(line, endpoint, targetObject) {
615
+ if (line.controlX !== undefined && line.controlY !== undefined) {
616
+ return this.computeCurvedClipInfo(line, endpoint, targetObject);
617
+ }
618
+ return this.computeStraightClipInfo(line, endpoint, targetObject);
619
+ }
620
+ computeStraightClipInfo(line, endpoint, targetObject) {
621
+ const reference = endpoint === 'start'
622
+ ? this.lineLocalToWorld(line, line.endX, line.endY)
623
+ : this.lineLocalToWorld(line, line.startX, line.startY);
624
+ const edgeIntersection = KritzelGeometryHelper.getLinePolygonIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, targetObject.rotatedPolygon);
625
+ if (!edgeIntersection) {
626
+ return null;
627
+ }
628
+ const localEdge = this.lineWorldToLocal(line, edgeIntersection.x, edgeIntersection.y);
629
+ const totalLength = Math.sqrt(Math.pow(line.endX - line.startX, 2) +
630
+ Math.pow(line.endY - line.startY, 2));
631
+ const distanceFromStart = Math.sqrt(Math.pow(localEdge.x - line.startX, 2) +
632
+ Math.pow(localEdge.y - line.startY, 2));
633
+ const t = totalLength > 0 ? distanceFromStart / totalLength : endpoint === 'start' ? 0 : 1;
634
+ return {
635
+ localX: localEdge.x,
636
+ localY: localEdge.y,
637
+ worldX: edgeIntersection.x,
638
+ worldY: edgeIntersection.y,
639
+ t,
640
+ };
641
+ }
642
+ computeCurvedClipInfo(line, endpoint, targetObject) {
643
+ const polygonPoints = this.getPolygonPoints(targetObject.rotatedPolygon);
644
+ const exitPoint = this.findCurveExitPoint(line, endpoint, polygonPoints);
645
+ if (exitPoint) {
646
+ return exitPoint;
647
+ }
648
+ const reference = endpoint === 'start'
649
+ ? this.lineLocalToWorld(line, line.endX, line.endY)
650
+ : this.lineLocalToWorld(line, line.startX, line.startY);
651
+ const fallbackIntersection = KritzelGeometryHelper.getLinePolygonIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, targetObject.rotatedPolygon);
652
+ if (!fallbackIntersection) {
653
+ return null;
654
+ }
655
+ const localEdge = this.lineWorldToLocal(line, fallbackIntersection.x, fallbackIntersection.y);
656
+ const approxT = this.approximateParameterForWorldPoint(line, fallbackIntersection);
657
+ return {
658
+ localX: localEdge.x,
659
+ localY: localEdge.y,
660
+ worldX: fallbackIntersection.x,
661
+ worldY: fallbackIntersection.y,
662
+ t: approxT,
663
+ };
664
+ }
665
+ findCurveExitPoint(line, endpoint, polygonPoints) {
666
+ const steps = 64;
667
+ const initialT = endpoint === 'start' ? 0 : 1;
668
+ const initialSample = this.evaluateLineAtT(line, initialT);
669
+ let prevInside = KritzelGeometryHelper.isPointInPolygon({ x: initialSample.worldX, y: initialSample.worldY }, polygonPoints);
670
+ let prevT = initialT;
671
+ for (let i = 1; i <= steps; i++) {
672
+ const t = endpoint === 'start' ? i / steps : 1 - i / steps;
673
+ const sample = this.evaluateLineAtT(line, t);
674
+ const inside = KritzelGeometryHelper.isPointInPolygon({ x: sample.worldX, y: sample.worldY }, polygonPoints);
675
+ if (prevInside && !inside) {
676
+ const refinedT = this.refineCurveExitParameter(line, polygonPoints, prevT, t);
677
+ const refinedPoint = this.evaluateLineAtT(line, refinedT);
678
+ return {
679
+ localX: refinedPoint.localX,
680
+ localY: refinedPoint.localY,
681
+ worldX: refinedPoint.worldX,
682
+ worldY: refinedPoint.worldY,
683
+ t: refinedT,
684
+ };
685
+ }
686
+ prevInside = inside;
687
+ prevT = t;
688
+ }
689
+ return null;
690
+ }
691
+ refineCurveExitParameter(line, polygonPoints, insideT, outsideT) {
692
+ let tInside = insideT;
693
+ let tOutside = outsideT;
694
+ for (let i = 0; i < 8; i++) {
695
+ const mid = (tInside + tOutside) / 2;
696
+ const sample = this.evaluateLineAtT(line, mid);
697
+ const inside = KritzelGeometryHelper.isPointInPolygon({ x: sample.worldX, y: sample.worldY }, polygonPoints);
698
+ if (inside) {
699
+ tInside = mid;
700
+ }
701
+ else {
702
+ tOutside = mid;
703
+ }
704
+ }
705
+ return (tInside + tOutside) / 2;
706
+ }
707
+ approximateParameterForWorldPoint(line, target) {
708
+ const steps = 80;
709
+ let bestT = 0;
710
+ let bestDistance = Infinity;
711
+ for (let i = 0; i <= steps; i++) {
712
+ const t = i / steps;
713
+ const sample = this.evaluateLineAtT(line, t);
714
+ const distance = Math.hypot(sample.worldX - target.x, sample.worldY - target.y);
715
+ if (distance < bestDistance) {
716
+ bestDistance = distance;
717
+ bestT = t;
718
+ }
719
+ }
720
+ return bestT;
721
+ }
722
+ evaluateLineAtT(line, t) {
723
+ const clampedT = Math.max(0, Math.min(1, t));
724
+ let localX;
725
+ let localY;
726
+ if (line.controlX !== undefined && line.controlY !== undefined) {
727
+ const oneMinusT = 1 - clampedT;
728
+ localX = oneMinusT * oneMinusT * line.startX + 2 * oneMinusT * clampedT * line.controlX + clampedT * clampedT * line.endX;
729
+ localY = oneMinusT * oneMinusT * line.startY + 2 * oneMinusT * clampedT * line.controlY + clampedT * clampedT * line.endY;
730
+ }
731
+ else {
732
+ localX = line.startX + (line.endX - line.startX) * clampedT;
733
+ localY = line.startY + (line.endY - line.startY) * clampedT;
734
+ }
735
+ const world = this.lineLocalToWorld(line, localX, localY);
736
+ return { t: clampedT, localX, localY, worldX: world.x, worldY: world.y };
737
+ }
738
+ evaluateDerivativeSpeedAtT(line, t) {
739
+ const clampedT = Math.max(0, Math.min(1, t));
740
+ if (line.controlX !== undefined && line.controlY !== undefined) {
741
+ // Quadratic Bezier derivative: B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
742
+ const p0x = line.startX;
743
+ const p0y = line.startY;
744
+ const p1x = line.controlX;
745
+ const p1y = line.controlY;
746
+ const p2x = line.endX;
747
+ const p2y = line.endY;
748
+ const dx = 2 * (1 - clampedT) * (p1x - p0x) + 2 * clampedT * (p2x - p1x);
749
+ const dy = 2 * (1 - clampedT) * (p1y - p0y) + 2 * clampedT * (p2y - p1y);
750
+ return Math.sqrt(dx * dx + dy * dy);
751
+ }
752
+ else {
753
+ // Straight line speed is constant length
754
+ const dx = line.endX - line.startX;
755
+ const dy = line.endY - line.startY;
756
+ return Math.sqrt(dx * dx + dy * dy);
757
+ }
758
+ }
759
+ extractQuadraticSegment(start, control, end, tStart, tEnd) {
760
+ let normalizedStart = Math.max(0, Math.min(1, tStart));
761
+ let normalizedEnd = Math.max(0, Math.min(1, tEnd));
762
+ if (normalizedEnd < normalizedStart) {
763
+ const swap = normalizedStart;
764
+ normalizedStart = normalizedEnd;
765
+ normalizedEnd = swap;
766
+ }
767
+ let segment = { start, control, end };
768
+ if (normalizedStart > 0) {
769
+ const split = this.splitQuadraticSegment(segment, normalizedStart);
770
+ segment = split.right;
771
+ const remainingRange = 1 - normalizedStart;
772
+ normalizedEnd = remainingRange > 0 ? (normalizedEnd - normalizedStart) / remainingRange : 1;
773
+ }
774
+ if (normalizedEnd < 1) {
775
+ const split = this.splitQuadraticSegment(segment, normalizedEnd);
776
+ segment = split.left;
777
+ }
778
+ return segment;
779
+ }
780
+ splitQuadraticSegment(segment, t) {
781
+ const clampedT = Math.max(0, Math.min(1, t));
782
+ const p0 = segment.start;
783
+ const p1 = segment.control;
784
+ const p2 = segment.end;
785
+ const p01 = this.lerpPoint(p0, p1, clampedT);
786
+ const p12 = this.lerpPoint(p1, p2, clampedT);
787
+ const p012 = this.lerpPoint(p01, p12, clampedT);
788
+ return {
789
+ left: { start: p0, control: p01, end: p012 },
790
+ right: { start: p012, control: p12, end: p2 },
791
+ };
792
+ }
793
+ lerpPoint(a, b, t) {
794
+ return {
795
+ x: a.x + (b.x - a.x) * t,
796
+ y: a.y + (b.y - a.y) * t,
797
+ };
798
+ }
799
+ buildAnchorPath(line, endpoint, clipInfo, targetObject) {
800
+ if (line.controlX === undefined || line.controlY === undefined || clipInfo.t === undefined) {
801
+ return `M ${clipInfo.worldX} ${clipInfo.worldY} L ${targetObject.centerX} ${targetObject.centerY}`;
802
+ }
803
+ const startT = endpoint === 'start' ? 0 : clipInfo.t;
804
+ const endT = endpoint === 'start' ? clipInfo.t : 1;
805
+ if (endT <= startT) {
806
+ return `M ${clipInfo.worldX} ${clipInfo.worldY} L ${targetObject.centerX} ${targetObject.centerY}`;
807
+ }
808
+ const segment = this.extractQuadraticSegment({ x: line.startX, y: line.startY }, { x: line.controlX, y: line.controlY }, { x: line.endX, y: line.endY }, startT, endT);
809
+ const reverse = endpoint === 'start';
810
+ return this.buildWorldQuadraticPath(line, segment, reverse);
811
+ }
812
+ buildWorldQuadraticPath(line, segment, reverse = false) {
813
+ const startPoint = reverse ? segment.end : segment.start;
814
+ const endPoint = reverse ? segment.start : segment.end;
815
+ const controlPoint = segment.control;
816
+ const startWorld = this.lineLocalToWorld(line, startPoint.x, startPoint.y);
817
+ const controlWorld = this.lineLocalToWorld(line, controlPoint.x, controlPoint.y);
818
+ const endWorld = this.lineLocalToWorld(line, endPoint.x, endPoint.y);
819
+ return `M ${startWorld.x} ${startWorld.y} Q ${controlWorld.x} ${controlWorld.y} ${endWorld.x} ${endWorld.y}`;
820
+ }
821
+ getPolygonPoints(polygon) {
822
+ return [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
823
+ }
824
+ /**
825
+ * Converts local line coordinates to world coordinates.
826
+ */
827
+ lineLocalToWorld(line, localX, localY) {
828
+ const px = localX - line.x;
829
+ const py = localY - line.y;
830
+ const cx = line.totalWidth / 2;
831
+ const cy = line.totalHeight / 2;
832
+ const cos = Math.cos(line.rotation);
833
+ const sin = Math.sin(line.rotation);
834
+ const rotatedX = (px - cx) * cos - (py - cy) * sin + cx;
835
+ const rotatedY = (px - cx) * sin + (py - cy) * cos + cy;
836
+ return {
837
+ x: rotatedX / line.scale + line.translateX,
838
+ y: rotatedY / line.scale + line.translateY,
839
+ };
840
+ }
841
+ /**
842
+ * Converts world coordinates to line's local SVG coordinates.
843
+ */
844
+ lineWorldToLocal(line, worldX, worldY) {
845
+ const dx = (worldX - line.translateX) * line.scale;
846
+ const dy = (worldY - line.translateY) * line.scale;
847
+ const cx = line.totalWidth / 2;
848
+ const cy = line.totalHeight / 2;
849
+ const cos = Math.cos(-line.rotation);
850
+ const sin = Math.sin(-line.rotation);
851
+ const rotatedX = (dx - cx) * cos - (dy - cy) * sin + cx;
852
+ const rotatedY = (dx - cx) * sin + (dy - cy) * cos + cy;
853
+ return {
854
+ x: rotatedX + line.x,
855
+ y: rotatedY + line.y,
856
+ };
857
+ }
858
+ /**
859
+ * Checks if an object can be used as an anchor target.
860
+ */
861
+ isAnchorable(object) {
862
+ // Exclude selection-related objects
863
+ if (object instanceof KritzelSelectionBox || object instanceof KritzelSelectionGroup) {
864
+ return false;
865
+ }
866
+ // Exclude line objects - lines cannot be anchored to other lines
867
+ if (object instanceof KritzelLine) {
868
+ return false;
869
+ }
870
+ // All other visible objects can be anchored to
871
+ return object.isVisible;
872
+ }
873
+ }
874
+ //# sourceMappingURL=anchor.manager.js.map