kritzel-stencil 0.3.25 → 0.3.27
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 +13 -8
- package/dist/cjs/{schema.constants-Np7j2Gz4.js → schema.constants-9v4Edzoe.js} +289 -108
- 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/resize.handler.js +22 -0
- package/dist/collection/classes/handlers/rotation.handler.js +18 -6
- package/dist/collection/classes/managers/anchor.manager.js +108 -45
- package/dist/collection/classes/objects/selection-group.class.js +80 -5
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +10 -6
- 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-B5XGjTLd.js → p-BmKYyPpl.js} +1 -1
- package/dist/components/{p-DNDt6O6A.js → p-CRKezkZU.js} +1 -1
- package/dist/components/{p-Dw-t1qQc.js → p-DXYkgNAO.js} +1 -1
- package/dist/components/{p-DF0S4FqL.js → p-G3xSPZ0x.js} +1 -1
- package/dist/components/p-ioK7ttPL.js +9 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/kritzel-active-users_45.entry.js +13 -8
- package/dist/esm/{schema.constants-1SDhlkfS.js → schema.constants-BT66X_r5.js} +289 -108
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-9ca04088.entry.js +9 -0
- package/dist/stencil/{p-1SDhlkfS.js → p-BT66X_r5.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/resize.handler.d.ts +7 -0
- package/dist/types/classes/handlers/rotation.handler.d.ts +4 -0
- package/dist/types/classes/managers/anchor.manager.d.ts +35 -2
- 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-6_95PFq4.js +0 -9
- package/dist/stencil/p-7ba7df62.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;
|
|
@@ -36,6 +36,22 @@ export class KritzelResizeHandler extends KritzelBaseHandler {
|
|
|
36
36
|
this.newSize = { x: 0, y: 0, width: 0, height: 0 };
|
|
37
37
|
this.hasResized = false;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Rebase pointer and size baselines to the latest live selection bounds.
|
|
41
|
+
* This prevents drift when anchored lines to external objects expand/shrink
|
|
42
|
+
* the selection box while a resize gesture is in progress.
|
|
43
|
+
*/
|
|
44
|
+
rebaseResizeBaseline(selectionGroup, clientX, clientY) {
|
|
45
|
+
this.initialMouseX = clientX;
|
|
46
|
+
this.initialMouseY = clientY;
|
|
47
|
+
this.initialSize.width = selectionGroup.width;
|
|
48
|
+
this.initialSize.height = selectionGroup.height;
|
|
49
|
+
this.initialSize.x = selectionGroup.translateX;
|
|
50
|
+
this.initialSize.y = selectionGroup.translateY;
|
|
51
|
+
}
|
|
52
|
+
shouldRebaseResizeBaseline(selectionGroup) {
|
|
53
|
+
return selectionGroup.hasExternalAnchoredLineConnections?.() === true;
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* Handles the pointer down event to initiate a resize operation.
|
|
41
57
|
* Captures the initial mouse position and selection group dimensions when
|
|
@@ -163,6 +179,9 @@ export class KritzelResizeHandler extends KritzelBaseHandler {
|
|
|
163
179
|
this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
|
|
164
180
|
this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
|
|
165
181
|
selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
|
|
182
|
+
if (this.shouldRebaseResizeBaseline(selectionGroup)) {
|
|
183
|
+
this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
|
|
184
|
+
}
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
if (event.pointerType === 'touch' || event.pointerType === 'pen') {
|
|
@@ -229,6 +248,9 @@ export class KritzelResizeHandler extends KritzelBaseHandler {
|
|
|
229
248
|
this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
|
|
230
249
|
this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
|
|
231
250
|
selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
|
|
251
|
+
if (this.shouldRebaseResizeBaseline(selectionGroup)) {
|
|
252
|
+
this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
|
|
253
|
+
}
|
|
232
254
|
}
|
|
233
255
|
}
|
|
234
256
|
}
|
|
@@ -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.
|
|
@@ -209,8 +217,25 @@ export class KritzelAnchorManager {
|
|
|
209
217
|
* @param worldY - Y coordinate in world space
|
|
210
218
|
* @param excludeLineId - ID of the line being edited (to exclude from snap targets)
|
|
211
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.
|
|
221
|
+
*/
|
|
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.
|
|
212
234
|
*/
|
|
213
|
-
|
|
235
|
+
findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
|
|
236
|
+
return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
|
|
237
|
+
}
|
|
238
|
+
findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
|
|
214
239
|
let bestTarget = null;
|
|
215
240
|
let highestZIndex = -Infinity;
|
|
216
241
|
const objects = this._core.store.allNonSelectionObjects;
|
|
@@ -230,28 +255,30 @@ export class KritzelAnchorManager {
|
|
|
230
255
|
// Check if point is inside the object's rotated polygon
|
|
231
256
|
const polygon = object.rotatedPolygon;
|
|
232
257
|
const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
|
|
233
|
-
if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
objectId: object.id,
|
|
247
|
-
centerX: object.centerX,
|
|
248
|
-
centerY: object.centerY,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
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
|
+
};
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
return bestTarget;
|
|
254
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
|
+
}
|
|
255
282
|
/**
|
|
256
283
|
* Sets the current snap candidate for visual feedback during line endpoint dragging.
|
|
257
284
|
* Updates the store state and triggers a rerender to show the snap indicator.
|
|
@@ -262,6 +289,17 @@ export class KritzelAnchorManager {
|
|
|
262
289
|
this._core.store.state.snapCandidate = candidate;
|
|
263
290
|
this._core.rerender();
|
|
264
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
|
+
}
|
|
265
303
|
/**
|
|
266
304
|
* Gets the current snap candidate from the store state.
|
|
267
305
|
*
|
|
@@ -270,6 +308,14 @@ export class KritzelAnchorManager {
|
|
|
270
308
|
getSnapCandidate() {
|
|
271
309
|
return this._core.store.state.snapCandidate ?? null;
|
|
272
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
|
+
}
|
|
273
319
|
/**
|
|
274
320
|
* Clears the snap candidate from the store state and triggers a rerender.
|
|
275
321
|
* Should be called when snapping is cancelled or completed.
|
|
@@ -278,6 +324,13 @@ export class KritzelAnchorManager {
|
|
|
278
324
|
this._core.store.state.snapCandidate = null;
|
|
279
325
|
this._core.rerender();
|
|
280
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
|
+
}
|
|
281
334
|
// ============================================
|
|
282
335
|
// Render Data
|
|
283
336
|
// ============================================
|
|
@@ -331,33 +384,43 @@ export class KritzelAnchorManager {
|
|
|
331
384
|
* or null if there's no active snap candidate.
|
|
332
385
|
*/
|
|
333
386
|
getSnapIndicatorRenderData() {
|
|
334
|
-
|
|
335
|
-
|
|
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)
|
|
336
399
|
return null;
|
|
337
|
-
if (!this.areFiniteNumbers(
|
|
400
|
+
if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
|
|
338
401
|
return null;
|
|
339
402
|
}
|
|
340
403
|
const scale = this.getSafeScale(this._core.store.state.scale);
|
|
341
404
|
const indicatorRadius = 8 / scale;
|
|
342
405
|
const indicatorStrokeWidth = `${2 / scale}`;
|
|
343
|
-
const lineStrokeWidthNum = this.areFiniteNumbers(
|
|
344
|
-
?
|
|
406
|
+
const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
|
|
407
|
+
? candidate.lineStrokeWidth
|
|
345
408
|
: (4 / scale);
|
|
346
409
|
const lineStrokeWidth = `${lineStrokeWidthNum}`;
|
|
347
410
|
const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
|
|
348
411
|
const dashArray = `${dashLength} ${dashLength}`;
|
|
349
|
-
const lineStroke =
|
|
350
|
-
let edgeX = this.areFiniteNumbers(
|
|
351
|
-
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;
|
|
352
415
|
let solidLineEndX = edgeX;
|
|
353
416
|
let solidLineEndY = edgeY;
|
|
354
417
|
let arrowPoints;
|
|
355
|
-
const arrowOffset = this.areFiniteNumbers(
|
|
356
|
-
?
|
|
418
|
+
const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
|
|
419
|
+
? candidate.arrowOffset
|
|
357
420
|
: undefined;
|
|
358
421
|
if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
|
|
359
|
-
const dx =
|
|
360
|
-
const dy =
|
|
422
|
+
const dx = candidate.lineEndpointX - edgeX;
|
|
423
|
+
const dy = candidate.lineEndpointY - edgeY;
|
|
361
424
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
362
425
|
if (length > arrowOffset) {
|
|
363
426
|
solidLineEndX = edgeX + (dx / length) * arrowOffset;
|
|
@@ -365,8 +428,8 @@ export class KritzelAnchorManager {
|
|
|
365
428
|
}
|
|
366
429
|
// Calculate arrow head points
|
|
367
430
|
// Direction from line endpoint to edge (arrow direction)
|
|
368
|
-
const arrowDx = edgeX -
|
|
369
|
-
const arrowDy = edgeY -
|
|
431
|
+
const arrowDx = edgeX - candidate.lineEndpointX;
|
|
432
|
+
const arrowDy = edgeY - candidate.lineEndpointY;
|
|
370
433
|
const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
|
|
371
434
|
if (arrowLengthTotal > 0) {
|
|
372
435
|
const ux = arrowDx / arrowLengthTotal;
|
|
@@ -397,15 +460,15 @@ export class KritzelAnchorManager {
|
|
|
397
460
|
return null;
|
|
398
461
|
}
|
|
399
462
|
const snapLinePath = (() => {
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.areFiniteNumbers(
|
|
404
|
-
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;
|
|
405
468
|
// Ensure meaningful range
|
|
406
469
|
if (startT >= 1)
|
|
407
470
|
return undefined;
|
|
408
|
-
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);
|
|
409
472
|
if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
|
|
410
473
|
return undefined;
|
|
411
474
|
}
|
|
@@ -419,15 +482,15 @@ export class KritzelAnchorManager {
|
|
|
419
482
|
lineStrokeWidth,
|
|
420
483
|
dashArray,
|
|
421
484
|
lineStroke,
|
|
422
|
-
centerX:
|
|
423
|
-
centerY:
|
|
424
|
-
lineEndpointX:
|
|
425
|
-
lineEndpointY:
|
|
485
|
+
centerX: candidate.centerX,
|
|
486
|
+
centerY: candidate.centerY,
|
|
487
|
+
lineEndpointX: candidate.lineEndpointX,
|
|
488
|
+
lineEndpointY: candidate.lineEndpointY,
|
|
426
489
|
edgeX,
|
|
427
490
|
edgeY,
|
|
428
491
|
arrowOffset,
|
|
429
|
-
arrowStyle:
|
|
430
|
-
arrowFill:
|
|
492
|
+
arrowStyle: candidate.arrowStyle,
|
|
493
|
+
arrowFill: candidate.arrowFill,
|
|
431
494
|
solidLineEndX,
|
|
432
495
|
solidLineEndY,
|
|
433
496
|
arrowPoints,
|