kritzel-stencil 0.3.24 → 0.3.26
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/cjs/index.cjs.js +1 -1
- package/dist/cjs/kritzel-active-users_45.cjs.entry.js +60 -36
- package/dist/cjs/{schema.constants-D8JoUfNc.js → schema.constants-CPz_o--2.js} +271 -101
- package/dist/collection/classes/handlers/line-handle.handler.js +59 -52
- package/dist/collection/classes/handlers/move.handler.js +2 -0
- package/dist/collection/classes/handlers/rotation.handler.js +18 -6
- package/dist/collection/classes/managers/anchor.manager.js +112 -38
- package/dist/collection/classes/managers/clipboard.manager.js +7 -0
- package/dist/collection/classes/objects/selection-group.class.js +80 -5
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +50 -34
- package/dist/collection/configs/default-engine-config.js +1 -0
- package/dist/collection/constants/version.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/kritzel-controls.js +1 -1
- package/dist/components/kritzel-editor.js +1 -1
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-settings.js +1 -1
- package/dist/components/kritzel-tool-config.js +1 -1
- package/dist/components/p-B0VgBZUT.js +9 -0
- package/dist/components/{p-Bz_zj6dQ.js → p-BAhZcKRe.js} +1 -1
- package/dist/components/{p-Dw-t1qQc.js → p-DKxt_AjI.js} +1 -1
- package/dist/components/{p-B5XGjTLd.js → p-DRncK1E6.js} +1 -1
- package/dist/components/{p-DNDt6O6A.js → p-DUo2M5uM.js} +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/kritzel-active-users_45.entry.js +60 -36
- package/dist/esm/{schema.constants-B3IqnD8l.js → schema.constants-C9ZX7hQv.js} +271 -101
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-71271db1.entry.js +9 -0
- package/dist/stencil/{p-B3IqnD8l.js → p-C9ZX7hQv.js} +1 -1
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/handlers/line-handle.handler.d.ts +1 -0
- package/dist/types/classes/handlers/rotation.handler.d.ts +4 -0
- package/dist/types/classes/managers/anchor.manager.d.ts +39 -3
- package/dist/types/classes/objects/selection-group.class.d.ts +8 -1
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/interfaces/anchor.interface.d.ts +12 -2
- package/dist/types/interfaces/engine-state.interface.d.ts +2 -1
- package/package.json +1 -1
- package/dist/components/p-on11Z8cZ.js +0 -9
- package/dist/stencil/p-63bc234f.entry.js +0 -9
|
@@ -47,6 +47,7 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
47
47
|
this.hasMoved = false;
|
|
48
48
|
this.currentSnapTarget = null;
|
|
49
49
|
this._core.anchorManager.clearSnapCandidate();
|
|
50
|
+
this._core.anchorManager.clearSnapPreviewCandidate();
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Handles pointer down events to initiate line handle dragging.
|
|
@@ -149,15 +150,19 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
149
150
|
const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
|
|
150
151
|
// Get other endpoint's anchor to prevent anchoring both to same object
|
|
151
152
|
const otherAnchorId = line.endAnchor?.objectId;
|
|
153
|
+
const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
|
|
152
154
|
// Check for snap target
|
|
153
|
-
const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
|
|
155
|
+
const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
|
|
154
156
|
this.currentSnapTarget = snapTarget;
|
|
157
|
+
const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
|
|
158
|
+
let controlWorld = undefined;
|
|
159
|
+
if (line.controlX !== undefined && line.controlY !== undefined) {
|
|
160
|
+
controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
|
|
161
|
+
}
|
|
155
162
|
if (snapTarget) {
|
|
156
163
|
// Snap to target center - convert world coords to local
|
|
157
164
|
const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
|
|
158
165
|
this.updateLineEndpoint(line, localCoords.x, localCoords.y, this.initialEndX, this.initialEndY);
|
|
159
|
-
// Get the other endpoint's world position for edge intersection calculation
|
|
160
|
-
const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
|
|
161
166
|
// Calculate edge intersection point using AnchorManager to support curves
|
|
162
167
|
const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
|
|
163
168
|
let edgeX;
|
|
@@ -171,30 +176,16 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
171
176
|
t = clipInfo.t;
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
|
-
// Calculate control point in world coordinates if it exists
|
|
175
|
-
let controlWorld = undefined;
|
|
176
|
-
if (line.controlX !== undefined && line.controlY !== undefined) {
|
|
177
|
-
controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
|
|
178
|
-
}
|
|
179
179
|
// Update snap indicator
|
|
180
|
-
this._core.anchorManager.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
t,
|
|
190
|
-
edgeX,
|
|
191
|
-
edgeY,
|
|
192
|
-
lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
|
|
193
|
-
lineStrokeWidth: line.strokeWidth / line.scale,
|
|
194
|
-
arrowOffset: line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined,
|
|
195
|
-
arrowStyle: line.hasStartArrow ? line.arrows?.start?.style : undefined,
|
|
196
|
-
arrowFill: line.hasStartArrow ? line.getArrowFill('start') : undefined,
|
|
197
|
-
});
|
|
180
|
+
this._core.anchorManager.setSnapPreviewCandidate(null);
|
|
181
|
+
this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'start', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
|
|
182
|
+
}
|
|
183
|
+
else if (previewTarget) {
|
|
184
|
+
const newStartX = this.initialStartX + localDx;
|
|
185
|
+
const newStartY = this.initialStartY + localDy;
|
|
186
|
+
this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
|
|
187
|
+
this._core.anchorManager.clearSnapCandidate();
|
|
188
|
+
this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'start', previewTarget, otherEndpointWorld, controlWorld));
|
|
198
189
|
}
|
|
199
190
|
else {
|
|
200
191
|
// No snap - use regular position
|
|
@@ -203,6 +194,7 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
203
194
|
this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
|
|
204
195
|
// Clear snap indicator
|
|
205
196
|
this._core.anchorManager.clearSnapCandidate();
|
|
197
|
+
this._core.anchorManager.clearSnapPreviewCandidate();
|
|
206
198
|
}
|
|
207
199
|
}
|
|
208
200
|
else if (handleType === 'end') {
|
|
@@ -211,15 +203,19 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
211
203
|
const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
|
|
212
204
|
// Get other endpoint's anchor to prevent anchoring both to same object
|
|
213
205
|
const otherAnchorId = line.startAnchor?.objectId;
|
|
206
|
+
const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
|
|
214
207
|
// Check for snap target
|
|
215
|
-
const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
|
|
208
|
+
const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
|
|
216
209
|
this.currentSnapTarget = snapTarget;
|
|
210
|
+
const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
|
|
211
|
+
let controlWorld = undefined;
|
|
212
|
+
if (line.controlX !== undefined && line.controlY !== undefined) {
|
|
213
|
+
controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
|
|
214
|
+
}
|
|
217
215
|
if (snapTarget) {
|
|
218
216
|
// Snap to target center - convert world coords to local
|
|
219
217
|
const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
|
|
220
218
|
this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, localCoords.x, localCoords.y);
|
|
221
|
-
// Get the other endpoint's world position for edge intersection calculation
|
|
222
|
-
const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
|
|
223
219
|
// Calculate edge intersection point using AnchorManager to support curves
|
|
224
220
|
const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
|
|
225
221
|
let edgeX;
|
|
@@ -233,30 +229,16 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
233
229
|
t = clipInfo.t;
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
|
-
// Calculate control point in world coordinates if it exists
|
|
237
|
-
let controlWorld = undefined;
|
|
238
|
-
if (line.controlX !== undefined && line.controlY !== undefined) {
|
|
239
|
-
controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
|
|
240
|
-
}
|
|
241
232
|
// Update snap indicator
|
|
242
|
-
this._core.anchorManager.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
t,
|
|
252
|
-
edgeX,
|
|
253
|
-
edgeY,
|
|
254
|
-
lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
|
|
255
|
-
lineStrokeWidth: line.strokeWidth / line.scale,
|
|
256
|
-
arrowOffset: line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined,
|
|
257
|
-
arrowStyle: line.hasEndArrow ? line.arrows?.end?.style : undefined,
|
|
258
|
-
arrowFill: line.hasEndArrow ? line.getArrowFill('end') : undefined,
|
|
259
|
-
});
|
|
233
|
+
this._core.anchorManager.setSnapPreviewCandidate(null);
|
|
234
|
+
this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'end', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
|
|
235
|
+
}
|
|
236
|
+
else if (previewTarget) {
|
|
237
|
+
const newEndX = this.initialEndX + localDx;
|
|
238
|
+
const newEndY = this.initialEndY + localDy;
|
|
239
|
+
this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
|
|
240
|
+
this._core.anchorManager.clearSnapCandidate();
|
|
241
|
+
this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'end', previewTarget, otherEndpointWorld, controlWorld));
|
|
260
242
|
}
|
|
261
243
|
else {
|
|
262
244
|
// No snap - use regular position
|
|
@@ -265,9 +247,12 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
265
247
|
this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
|
|
266
248
|
// Clear snap indicator
|
|
267
249
|
this._core.anchorManager.clearSnapCandidate();
|
|
250
|
+
this._core.anchorManager.clearSnapPreviewCandidate();
|
|
268
251
|
}
|
|
269
252
|
}
|
|
270
253
|
else if (handleType === 'center') {
|
|
254
|
+
this._core.anchorManager.clearSnapPreviewCandidate();
|
|
255
|
+
this._core.anchorManager.clearSnapCandidate();
|
|
271
256
|
const startControlX = this.initialControlX ?? (this.initialStartX + this.initialEndX) / 2;
|
|
272
257
|
const startControlY = this.initialControlY ?? (this.initialStartY + this.initialEndY) / 2;
|
|
273
258
|
const newControlX = startControlX + localDx * 2;
|
|
@@ -463,4 +448,26 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
|
|
|
463
448
|
}
|
|
464
449
|
return null;
|
|
465
450
|
}
|
|
451
|
+
createIndicatorCandidate(line, endpoint, target, otherEndpointWorld, controlWorld, clipInfo) {
|
|
452
|
+
return {
|
|
453
|
+
objectId: target.objectId,
|
|
454
|
+
endpoint,
|
|
455
|
+
centerX: target.centerX,
|
|
456
|
+
centerY: target.centerY,
|
|
457
|
+
lineEndpointX: otherEndpointWorld.x,
|
|
458
|
+
lineEndpointY: otherEndpointWorld.y,
|
|
459
|
+
controlX: controlWorld?.x,
|
|
460
|
+
controlY: controlWorld?.y,
|
|
461
|
+
t: clipInfo?.t,
|
|
462
|
+
edgeX: clipInfo?.edgeX,
|
|
463
|
+
edgeY: clipInfo?.edgeY,
|
|
464
|
+
lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
|
|
465
|
+
lineStrokeWidth: line.strokeWidth / line.scale,
|
|
466
|
+
arrowOffset: endpoint === 'start'
|
|
467
|
+
? (line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined)
|
|
468
|
+
: (line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined),
|
|
469
|
+
arrowStyle: endpoint === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style,
|
|
470
|
+
arrowFill: endpoint === 'start' ? line.getArrowFill('start') : line.getArrowFill('end'),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
466
473
|
}
|
|
@@ -211,6 +211,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
|
|
|
211
211
|
this._core.store.state.isDragging = false;
|
|
212
212
|
if (this.hasMoved) {
|
|
213
213
|
this._core.store.selectionGroup.persistChildren();
|
|
214
|
+
this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
|
|
214
215
|
this._core.store.selectionGroup.update();
|
|
215
216
|
this._core.engine.emitObjectsChange();
|
|
216
217
|
this._core.store.state.hasObjectsChanged = true;
|
|
@@ -223,6 +224,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
|
|
|
223
224
|
this._core.store.state.isDragging = false;
|
|
224
225
|
if (this.hasMoved) {
|
|
225
226
|
this._core.store.selectionGroup.persistChildren();
|
|
227
|
+
this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
|
|
226
228
|
this._core.store.selectionGroup.update();
|
|
227
229
|
this._core.engine.emitObjectsChange();
|
|
228
230
|
this._core.store.state.hasObjectsChanged = true;
|
|
@@ -12,6 +12,10 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
12
12
|
rotation = 0;
|
|
13
13
|
/** The rotation value of the selection group before the current rotation operation began. */
|
|
14
14
|
initialSelectionGroupRotation = 0;
|
|
15
|
+
/** The X coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
|
|
16
|
+
initialCenterX = 0;
|
|
17
|
+
/** The Y coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
|
|
18
|
+
initialCenterY = 0;
|
|
15
19
|
/**
|
|
16
20
|
* Creates an instance of KritzelRotationHandler.
|
|
17
21
|
* @param core - The KritzelCore instance that provides access to the store, engine, and other core functionalities.
|
|
@@ -26,6 +30,8 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
26
30
|
reset() {
|
|
27
31
|
this.initialRotation = 0;
|
|
28
32
|
this.rotation = 0;
|
|
33
|
+
this.initialCenterX = 0;
|
|
34
|
+
this.initialCenterY = 0;
|
|
29
35
|
}
|
|
30
36
|
/**
|
|
31
37
|
* Handles the pointer down event to initiate a rotation operation.
|
|
@@ -45,6 +51,10 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
45
51
|
const objectScale = selectionGroup.scale || 1;
|
|
46
52
|
const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
|
|
47
53
|
const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
|
|
54
|
+
// Capture the pivot once so it stays fixed even if the bounding box grows
|
|
55
|
+
// mid-rotation (e.g. a line anchored to an object outside the selection).
|
|
56
|
+
this.initialCenterX = centerX;
|
|
57
|
+
this.initialCenterY = centerY;
|
|
48
58
|
const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
|
|
49
59
|
const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
|
|
50
60
|
this.initialSelectionGroupRotation = selectionGroup.rotation;
|
|
@@ -68,6 +78,10 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
68
78
|
const objectScale = selectionGroup.scale || 1;
|
|
69
79
|
const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
|
|
70
80
|
const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
|
|
81
|
+
// Capture the pivot once so it stays fixed even if the bounding box grows
|
|
82
|
+
// mid-rotation (e.g. a line anchored to an object outside the selection).
|
|
83
|
+
this.initialCenterX = centerX;
|
|
84
|
+
this.initialCenterY = centerY;
|
|
71
85
|
const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
|
|
72
86
|
const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
|
|
73
87
|
this.initialSelectionGroupRotation = selectionGroup.rotation;
|
|
@@ -92,9 +106,8 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
92
106
|
if (this._core.store.state.isRotating && selectionGroup) {
|
|
93
107
|
const clientX = event.clientX - this._core.store.offsetX;
|
|
94
108
|
const clientY = event.clientY - this._core.store.offsetY;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
|
|
109
|
+
const groupCenterX = this.initialCenterX;
|
|
110
|
+
const groupCenterY = this.initialCenterY;
|
|
98
111
|
const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
|
|
99
112
|
const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
|
|
100
113
|
const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
|
|
@@ -112,9 +125,8 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
|
|
|
112
125
|
if (this._core.store.state.isRotating && selectionGroup) {
|
|
113
126
|
const clientX = Math.round(firstTouch.clientX - this._core.store.offsetX);
|
|
114
127
|
const clientY = Math.round(firstTouch.clientY - this._core.store.offsetY);
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
|
|
128
|
+
const groupCenterX = this.initialCenterX;
|
|
129
|
+
const groupCenterY = this.initialCenterY;
|
|
118
130
|
const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
|
|
119
131
|
const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
|
|
120
132
|
const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
|
|
@@ -143,6 +143,14 @@ export class KritzelAnchorManager {
|
|
|
143
143
|
}
|
|
144
144
|
this.snapEndpointToObject(line, entry.endpoint, objectId);
|
|
145
145
|
}
|
|
146
|
+
const selectionGroup = this._core.store.selectionGroup;
|
|
147
|
+
if (!selectionGroup || !this._core.store.state.isDragging) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (selectionGroup.hasExternalAnchoredLineConnections()) {
|
|
151
|
+
// Keep the local drag overlay aligned with reshaped anchored lines without spamming Yjs.
|
|
152
|
+
selectionGroup.refreshObjectDimensions(undefined, true);
|
|
153
|
+
}
|
|
146
154
|
}
|
|
147
155
|
/**
|
|
148
156
|
* Snaps a line endpoint to an object's center position.
|
|
@@ -199,15 +207,35 @@ export class KritzelAnchorManager {
|
|
|
199
207
|
// Snap Detection
|
|
200
208
|
// ============================================
|
|
201
209
|
/**
|
|
202
|
-
* Finds
|
|
210
|
+
* Finds an object that can be snapped to when the cursor is sufficiently close to its center.
|
|
211
|
+
* Snap is only triggered when the cursor is inside the object's bounds AND within 1/5 of the
|
|
212
|
+
* shorter side from the center, so snapping activates only as the user approaches the center
|
|
213
|
+
* and releases quickly as they move away.
|
|
203
214
|
* Returns null if no suitable snap target is found.
|
|
204
215
|
*
|
|
205
216
|
* @param worldX - X coordinate in world space
|
|
206
217
|
* @param worldY - Y coordinate in world space
|
|
207
218
|
* @param excludeLineId - ID of the line being edited (to exclude from snap targets)
|
|
208
219
|
* @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
|
|
220
|
+
* @param pointerType - The pointer type driving the drag interaction.
|
|
209
221
|
*/
|
|
210
|
-
findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
|
|
222
|
+
findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, pointerType = 'mouse') {
|
|
223
|
+
return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, object => {
|
|
224
|
+
const snapRadius = this.getSnapRadius(object, pointerType);
|
|
225
|
+
const dx = worldX - object.centerX;
|
|
226
|
+
const dy = worldY - object.centerY;
|
|
227
|
+
const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
|
|
228
|
+
return distanceToCenter <= snapRadius;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Finds the topmost anchorable object currently containing the dragged endpoint.
|
|
233
|
+
* Used to drive preview UI before an actual snap engages.
|
|
234
|
+
*/
|
|
235
|
+
findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
|
|
236
|
+
return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
|
|
237
|
+
}
|
|
238
|
+
findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
|
|
211
239
|
let bestTarget = null;
|
|
212
240
|
let highestZIndex = -Infinity;
|
|
213
241
|
const objects = this._core.store.allNonSelectionObjects;
|
|
@@ -227,20 +255,30 @@ export class KritzelAnchorManager {
|
|
|
227
255
|
// Check if point is inside the object's rotated polygon
|
|
228
256
|
const polygon = object.rotatedPolygon;
|
|
229
257
|
const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
|
|
230
|
-
if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
258
|
+
if (!KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!predicate(object)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (object.zIndex > highestZIndex) {
|
|
265
|
+
highestZIndex = object.zIndex;
|
|
266
|
+
bestTarget = {
|
|
267
|
+
objectId: object.id,
|
|
268
|
+
centerX: object.centerX,
|
|
269
|
+
centerY: object.centerY,
|
|
270
|
+
};
|
|
240
271
|
}
|
|
241
272
|
}
|
|
242
273
|
return bestTarget;
|
|
243
274
|
}
|
|
275
|
+
getSnapRadius(object, pointerType) {
|
|
276
|
+
const baseRadius = Math.min(object.width, object.height) / 5;
|
|
277
|
+
if (pointerType === 'touch' || pointerType === 'pen') {
|
|
278
|
+
return Math.max(baseRadius * 1.75, 24 / this.getSafeScale(this._core.store.state.scale));
|
|
279
|
+
}
|
|
280
|
+
return baseRadius;
|
|
281
|
+
}
|
|
244
282
|
/**
|
|
245
283
|
* Sets the current snap candidate for visual feedback during line endpoint dragging.
|
|
246
284
|
* Updates the store state and triggers a rerender to show the snap indicator.
|
|
@@ -251,6 +289,17 @@ export class KritzelAnchorManager {
|
|
|
251
289
|
this._core.store.state.snapCandidate = candidate;
|
|
252
290
|
this._core.rerender();
|
|
253
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Sets the current snap preview candidate for early visual feedback during line dragging.
|
|
294
|
+
* Preview state must remain separate from active snap state so line clipping and anchor
|
|
295
|
+
* commit semantics only react to actual snaps.
|
|
296
|
+
*
|
|
297
|
+
* @param candidate - The preview candidate object, or null to clear.
|
|
298
|
+
*/
|
|
299
|
+
setSnapPreviewCandidate(candidate) {
|
|
300
|
+
this._core.store.state.snapPreviewCandidate = candidate;
|
|
301
|
+
this._core.rerender();
|
|
302
|
+
}
|
|
254
303
|
/**
|
|
255
304
|
* Gets the current snap candidate from the store state.
|
|
256
305
|
*
|
|
@@ -259,6 +308,14 @@ export class KritzelAnchorManager {
|
|
|
259
308
|
getSnapCandidate() {
|
|
260
309
|
return this._core.store.state.snapCandidate ?? null;
|
|
261
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Gets the current snap preview candidate from the store state.
|
|
313
|
+
*
|
|
314
|
+
* @returns The current preview candidate if one exists, null otherwise.
|
|
315
|
+
*/
|
|
316
|
+
getSnapPreviewCandidate() {
|
|
317
|
+
return this._core.store.state.snapPreviewCandidate ?? null;
|
|
318
|
+
}
|
|
262
319
|
/**
|
|
263
320
|
* Clears the snap candidate from the store state and triggers a rerender.
|
|
264
321
|
* Should be called when snapping is cancelled or completed.
|
|
@@ -267,6 +324,13 @@ export class KritzelAnchorManager {
|
|
|
267
324
|
this._core.store.state.snapCandidate = null;
|
|
268
325
|
this._core.rerender();
|
|
269
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Clears the snap preview candidate from the store state and triggers a rerender.
|
|
329
|
+
*/
|
|
330
|
+
clearSnapPreviewCandidate() {
|
|
331
|
+
this._core.store.state.snapPreviewCandidate = null;
|
|
332
|
+
this._core.rerender();
|
|
333
|
+
}
|
|
270
334
|
// ============================================
|
|
271
335
|
// Render Data
|
|
272
336
|
// ============================================
|
|
@@ -320,33 +384,43 @@ export class KritzelAnchorManager {
|
|
|
320
384
|
* or null if there's no active snap candidate.
|
|
321
385
|
*/
|
|
322
386
|
getSnapIndicatorRenderData() {
|
|
323
|
-
|
|
324
|
-
|
|
387
|
+
return this.getIndicatorRenderDataForCandidate(this.getSnapCandidate());
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Gets render data for snap preview visualization.
|
|
391
|
+
*
|
|
392
|
+
* @returns SnapIndicatorRenderData for the active preview candidate, or null if none exists.
|
|
393
|
+
*/
|
|
394
|
+
getSnapPreviewRenderData() {
|
|
395
|
+
return this.getIndicatorRenderDataForCandidate(this.getSnapPreviewCandidate());
|
|
396
|
+
}
|
|
397
|
+
getIndicatorRenderDataForCandidate(candidate) {
|
|
398
|
+
if (!candidate)
|
|
325
399
|
return null;
|
|
326
|
-
if (!this.areFiniteNumbers(
|
|
400
|
+
if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
|
|
327
401
|
return null;
|
|
328
402
|
}
|
|
329
403
|
const scale = this.getSafeScale(this._core.store.state.scale);
|
|
330
404
|
const indicatorRadius = 8 / scale;
|
|
331
405
|
const indicatorStrokeWidth = `${2 / scale}`;
|
|
332
|
-
const lineStrokeWidthNum = this.areFiniteNumbers(
|
|
333
|
-
?
|
|
406
|
+
const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
|
|
407
|
+
? candidate.lineStrokeWidth
|
|
334
408
|
: (4 / scale);
|
|
335
409
|
const lineStrokeWidth = `${lineStrokeWidthNum}`;
|
|
336
410
|
const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
|
|
337
411
|
const dashArray = `${dashLength} ${dashLength}`;
|
|
338
|
-
const lineStroke =
|
|
339
|
-
let edgeX = this.areFiniteNumbers(
|
|
340
|
-
let edgeY = this.areFiniteNumbers(
|
|
412
|
+
const lineStroke = candidate.lineStroke || '#000000';
|
|
413
|
+
let edgeX = this.areFiniteNumbers(candidate.edgeX) ? candidate.edgeX : undefined;
|
|
414
|
+
let edgeY = this.areFiniteNumbers(candidate.edgeY) ? candidate.edgeY : undefined;
|
|
341
415
|
let solidLineEndX = edgeX;
|
|
342
416
|
let solidLineEndY = edgeY;
|
|
343
417
|
let arrowPoints;
|
|
344
|
-
const arrowOffset = this.areFiniteNumbers(
|
|
345
|
-
?
|
|
418
|
+
const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
|
|
419
|
+
? candidate.arrowOffset
|
|
346
420
|
: undefined;
|
|
347
421
|
if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
|
|
348
|
-
const dx =
|
|
349
|
-
const dy =
|
|
422
|
+
const dx = candidate.lineEndpointX - edgeX;
|
|
423
|
+
const dy = candidate.lineEndpointY - edgeY;
|
|
350
424
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
351
425
|
if (length > arrowOffset) {
|
|
352
426
|
solidLineEndX = edgeX + (dx / length) * arrowOffset;
|
|
@@ -354,8 +428,8 @@ export class KritzelAnchorManager {
|
|
|
354
428
|
}
|
|
355
429
|
// Calculate arrow head points
|
|
356
430
|
// Direction from line endpoint to edge (arrow direction)
|
|
357
|
-
const arrowDx = edgeX -
|
|
358
|
-
const arrowDy = edgeY -
|
|
431
|
+
const arrowDx = edgeX - candidate.lineEndpointX;
|
|
432
|
+
const arrowDy = edgeY - candidate.lineEndpointY;
|
|
359
433
|
const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
|
|
360
434
|
if (arrowLengthTotal > 0) {
|
|
361
435
|
const ux = arrowDx / arrowLengthTotal;
|
|
@@ -386,15 +460,15 @@ export class KritzelAnchorManager {
|
|
|
386
460
|
return null;
|
|
387
461
|
}
|
|
388
462
|
const snapLinePath = (() => {
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.areFiniteNumbers(
|
|
393
|
-
const startT =
|
|
463
|
+
if (candidate.controlX !== undefined &&
|
|
464
|
+
candidate.controlY !== undefined &&
|
|
465
|
+
candidate.t !== undefined &&
|
|
466
|
+
this.areFiniteNumbers(candidate.controlX, candidate.controlY, candidate.t)) {
|
|
467
|
+
const startT = candidate.endpoint === 'start' ? 1 - candidate.t : candidate.t;
|
|
394
468
|
// Ensure meaningful range
|
|
395
469
|
if (startT >= 1)
|
|
396
470
|
return undefined;
|
|
397
|
-
const segment = this.extractQuadraticSegment({ x:
|
|
471
|
+
const segment = this.extractQuadraticSegment({ x: candidate.lineEndpointX, y: candidate.lineEndpointY }, { x: candidate.controlX, y: candidate.controlY }, { x: candidate.centerX, y: candidate.centerY }, startT, 1);
|
|
398
472
|
if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
|
|
399
473
|
return undefined;
|
|
400
474
|
}
|
|
@@ -408,15 +482,15 @@ export class KritzelAnchorManager {
|
|
|
408
482
|
lineStrokeWidth,
|
|
409
483
|
dashArray,
|
|
410
484
|
lineStroke,
|
|
411
|
-
centerX:
|
|
412
|
-
centerY:
|
|
413
|
-
lineEndpointX:
|
|
414
|
-
lineEndpointY:
|
|
485
|
+
centerX: candidate.centerX,
|
|
486
|
+
centerY: candidate.centerY,
|
|
487
|
+
lineEndpointX: candidate.lineEndpointX,
|
|
488
|
+
lineEndpointY: candidate.lineEndpointY,
|
|
415
489
|
edgeX,
|
|
416
490
|
edgeY,
|
|
417
491
|
arrowOffset,
|
|
418
|
-
arrowStyle:
|
|
419
|
-
arrowFill:
|
|
492
|
+
arrowStyle: candidate.arrowStyle,
|
|
493
|
+
arrowFill: candidate.arrowFill,
|
|
420
494
|
solidLineEndX,
|
|
421
495
|
solidLineEndY,
|
|
422
496
|
arrowPoints,
|
|
@@ -103,6 +103,7 @@ export class KritzelClipboardManager {
|
|
|
103
103
|
*/
|
|
104
104
|
regenerateIdsAndRemapAnchors(topLevel) {
|
|
105
105
|
const all = [];
|
|
106
|
+
const hasOnlyTopLevelLines = topLevel.length > 0 && topLevel.every(object => KritzelClassHelper.isInstanceOf(object, 'KritzelLine'));
|
|
106
107
|
const collect = (objects) => {
|
|
107
108
|
for (const object of objects) {
|
|
108
109
|
all.push(object);
|
|
@@ -127,9 +128,15 @@ export class KritzelClipboardManager {
|
|
|
127
128
|
if (object.startAnchor && idMap.has(object.startAnchor.objectId)) {
|
|
128
129
|
object.startAnchor = { objectId: idMap.get(object.startAnchor.objectId) };
|
|
129
130
|
}
|
|
131
|
+
else if (hasOnlyTopLevelLines) {
|
|
132
|
+
object.startAnchor = undefined;
|
|
133
|
+
}
|
|
130
134
|
if (object.endAnchor && idMap.has(object.endAnchor.objectId)) {
|
|
131
135
|
object.endAnchor = { objectId: idMap.get(object.endAnchor.objectId) };
|
|
132
136
|
}
|
|
137
|
+
else if (hasOnlyTopLevelLines) {
|
|
138
|
+
object.endAnchor = undefined;
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
141
|
}
|
|
135
142
|
}
|