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.
Files changed (40) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_45.cjs.entry.js +60 -36
  3. package/dist/cjs/{schema.constants-D8JoUfNc.js → schema.constants-CPz_o--2.js} +271 -101
  4. package/dist/collection/classes/handlers/line-handle.handler.js +59 -52
  5. package/dist/collection/classes/handlers/move.handler.js +2 -0
  6. package/dist/collection/classes/handlers/rotation.handler.js +18 -6
  7. package/dist/collection/classes/managers/anchor.manager.js +112 -38
  8. package/dist/collection/classes/managers/clipboard.manager.js +7 -0
  9. package/dist/collection/classes/objects/selection-group.class.js +80 -5
  10. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +50 -34
  11. package/dist/collection/configs/default-engine-config.js +1 -0
  12. package/dist/collection/constants/version.js +1 -1
  13. package/dist/components/index.js +1 -1
  14. package/dist/components/kritzel-controls.js +1 -1
  15. package/dist/components/kritzel-editor.js +1 -1
  16. package/dist/components/kritzel-engine.js +1 -1
  17. package/dist/components/kritzel-settings.js +1 -1
  18. package/dist/components/kritzel-tool-config.js +1 -1
  19. package/dist/components/p-B0VgBZUT.js +9 -0
  20. package/dist/components/{p-Bz_zj6dQ.js → p-BAhZcKRe.js} +1 -1
  21. package/dist/components/{p-Dw-t1qQc.js → p-DKxt_AjI.js} +1 -1
  22. package/dist/components/{p-B5XGjTLd.js → p-DRncK1E6.js} +1 -1
  23. package/dist/components/{p-DNDt6O6A.js → p-DUo2M5uM.js} +1 -1
  24. package/dist/esm/index.js +2 -2
  25. package/dist/esm/kritzel-active-users_45.entry.js +60 -36
  26. package/dist/esm/{schema.constants-B3IqnD8l.js → schema.constants-C9ZX7hQv.js} +271 -101
  27. package/dist/stencil/index.esm.js +1 -1
  28. package/dist/stencil/p-71271db1.entry.js +9 -0
  29. package/dist/stencil/{p-B3IqnD8l.js → p-C9ZX7hQv.js} +1 -1
  30. package/dist/stencil/stencil.esm.js +1 -1
  31. package/dist/types/classes/handlers/line-handle.handler.d.ts +1 -0
  32. package/dist/types/classes/handlers/rotation.handler.d.ts +4 -0
  33. package/dist/types/classes/managers/anchor.manager.d.ts +39 -3
  34. package/dist/types/classes/objects/selection-group.class.d.ts +8 -1
  35. package/dist/types/constants/version.d.ts +1 -1
  36. package/dist/types/interfaces/anchor.interface.d.ts +12 -2
  37. package/dist/types/interfaces/engine-state.interface.d.ts +2 -1
  38. package/package.json +1 -1
  39. package/dist/components/p-on11Z8cZ.js +0 -9
  40. package/dist/stencil/p-63bc234f.entry.js +0 -9
@@ -20704,6 +20704,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
20704
20704
  _lastChildPersistTime = 0;
20705
20705
  /** Minimum interval (ms) between child Yjs persists during drag */
20706
20706
  static CHILD_PERSIST_THROTTLE_MS = 100;
20707
+ /** Ignore tiny floating-point diffs when comparing refreshed bounds */
20708
+ static DIMENSION_EPSILON = 0.001;
20707
20709
  /**
20708
20710
  * Gets the array of object IDs contained in this selection group.
20709
20711
  * @returns Array of string IDs for the selected objects
@@ -21077,6 +21079,30 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21077
21079
  }
21078
21080
  });
21079
21081
  }
21082
+ /**
21083
+ * Returns true when the selection contains a line anchored to an object
21084
+ * that is not part of this selection group.
21085
+ */
21086
+ hasExternalAnchoredLineConnections(cachedObjects) {
21087
+ const children = cachedObjects ?? this.objects;
21088
+ if (children.length === 0) {
21089
+ return false;
21090
+ }
21091
+ const selectedIds = new Set(children.map(child => child.id));
21092
+ for (const child of children) {
21093
+ if (!KritzelClassHelper.isInstanceOf(child, 'KritzelLine')) {
21094
+ continue;
21095
+ }
21096
+ const line = child;
21097
+ if (line.startAnchor && !selectedIds.has(line.startAnchor.objectId)) {
21098
+ return true;
21099
+ }
21100
+ if (line.endAnchor && !selectedIds.has(line.endAnchor.objectId)) {
21101
+ return true;
21102
+ }
21103
+ }
21104
+ return false;
21105
+ }
21080
21106
  /**
21081
21107
  * Resizes the selection group and scales all contained objects proportionally.
21082
21108
  * Uses snapshot values to avoid compounding errors during continuous drag operations.
@@ -21169,8 +21195,13 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21169
21195
  */
21170
21196
  rotate(value) {
21171
21197
  this.rotation = value;
21172
- const centerX = this.translateX + this.totalWidth / 2 / this.scale;
21173
- const centerY = this.translateY + this.totalHeight / 2 / this.scale;
21198
+ // Use the snapshot center as the rotation pivot rather than the live center.
21199
+ // refreshObjectDimensions() below can grow the box when an anchored line is pinned
21200
+ // to an external object; keeping the pivot fixed prevents the children from drifting.
21201
+ const snapshotTotalWidth = this.snapshotWidth + this.padding * 2;
21202
+ const snapshotTotalHeight = this.snapshotHeight + this.padding * 2;
21203
+ const centerX = this.snapshotTranslateX + snapshotTotalWidth / 2 / this.scale;
21204
+ const centerY = this.snapshotTranslateY + snapshotTotalHeight / 2 / this.scale;
21174
21205
  const angle = value - this.snapshotRotation;
21175
21206
  const cos = Math.cos(angle);
21176
21207
  const sin = Math.sin(angle);
@@ -21192,6 +21223,31 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21192
21223
  child.translateY = centerY + rotatedY - child.totalHeight / 2 / child.scale;
21193
21224
  child.rotate(childCount === 1 ? value : unchangedSnapshot.rotation + angle);
21194
21225
  }
21226
+ // Re-sync any anchored lines so their stored geometry follows the rotated objects
21227
+ // (including lines anchored to an object outside this selection), then grow the
21228
+ // selection box to enclose the reshaped lines. Rotation sets state.isRotating
21229
+ // rather than state.isDragging, so the refresh must be triggered explicitly here.
21230
+ const anchorTargetIds = new Set();
21231
+ for (const child of children) {
21232
+ anchorTargetIds.add(child.id);
21233
+ if (KritzelClassHelper.isInstanceOf(child, 'KritzelLine')) {
21234
+ const line = child;
21235
+ if (line.startAnchor)
21236
+ anchorTargetIds.add(line.startAnchor.objectId);
21237
+ if (line.endAnchor)
21238
+ anchorTargetIds.add(line.endAnchor.objectId);
21239
+ }
21240
+ }
21241
+ let hasAnchoredLines = false;
21242
+ for (const targetId of anchorTargetIds) {
21243
+ if (this._core.anchorManager.getLinesAnchoredTo(targetId).length > 0) {
21244
+ hasAnchoredLines = true;
21245
+ this._core.anchorManager.updateAnchorsForObject(targetId);
21246
+ }
21247
+ }
21248
+ if (hasAnchoredLines) {
21249
+ this.refreshObjectDimensions(children);
21250
+ }
21195
21251
  });
21196
21252
  }
21197
21253
  /**
@@ -21221,6 +21277,16 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21221
21277
  * @param skipPersist If true, only updates local state without persisting to Yjs (used during undo/redo)
21222
21278
  */
21223
21279
  refreshObjectDimensions(cachedObjects, skipPersist = false) {
21280
+ const previous = {
21281
+ minX: this.minX,
21282
+ minY: this.minY,
21283
+ maxX: this.maxX,
21284
+ maxY: this.maxY,
21285
+ width: this.width,
21286
+ height: this.height,
21287
+ translateX: this.translateX,
21288
+ translateY: this.translateY,
21289
+ };
21224
21290
  const children = cachedObjects ?? this.objects;
21225
21291
  if (children.length === 1) {
21226
21292
  const obj = children[0];
@@ -21274,9 +21340,18 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21274
21340
  this.translateX = cx - (this.width / this.scale + 2 * this.padding) / 2;
21275
21341
  this.translateY = cy - (this.height / this.scale + 2 * this.padding) / 2;
21276
21342
  }
21277
- if (!skipPersist) {
21343
+ const hasChanged = Math.abs(this.minX - previous.minX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21344
+ Math.abs(this.minY - previous.minY) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21345
+ Math.abs(this.maxX - previous.maxX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21346
+ Math.abs(this.maxY - previous.maxY) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21347
+ Math.abs(this.width - previous.width) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21348
+ Math.abs(this.height - previous.height) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21349
+ Math.abs(this.translateX - previous.translateX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21350
+ Math.abs(this.translateY - previous.translateY) > KritzelSelectionGroup.DIMENSION_EPSILON;
21351
+ if (!skipPersist && hasChanged) {
21278
21352
  this._core.store.objects.update(this);
21279
21353
  }
21354
+ return hasChanged;
21280
21355
  }
21281
21356
  /**
21282
21357
  * Calculates the horizontal offset from the group's center to an object's center using snapshot data.
@@ -21286,7 +21361,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21286
21361
  */
21287
21362
  getOffsetXToCenterFromSnapshot(snapshot) {
21288
21363
  const objCenterX = snapshot.translateX + snapshot.totalWidth / snapshot.scale / 2;
21289
- const groupCenterX = this.translateX + this.totalWidth / this.scale / 2;
21364
+ const groupCenterX = this.snapshotTranslateX + (this.snapshotWidth + this.padding * 2) / this.scale / 2;
21290
21365
  return objCenterX - groupCenterX;
21291
21366
  }
21292
21367
  /**
@@ -21297,7 +21372,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21297
21372
  */
21298
21373
  getOffsetYToCenterFromSnapshot(snapshot) {
21299
21374
  const objCenterY = snapshot.translateY + snapshot.totalHeight / snapshot.scale / 2;
21300
- const groupCenterY = this.translateY + this.totalHeight / this.scale / 2;
21375
+ const groupCenterY = this.snapshotTranslateY + (this.snapshotHeight + this.padding * 2) / this.scale / 2;
21301
21376
  return objCenterY - groupCenterY;
21302
21377
  }
21303
21378
  /**
@@ -22670,6 +22745,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
22670
22745
  this._core.store.state.isDragging = false;
22671
22746
  if (this.hasMoved) {
22672
22747
  this._core.store.selectionGroup.persistChildren();
22748
+ this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
22673
22749
  this._core.store.selectionGroup.update();
22674
22750
  this._core.engine.emitObjectsChange();
22675
22751
  this._core.store.state.hasObjectsChanged = true;
@@ -22682,6 +22758,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
22682
22758
  this._core.store.state.isDragging = false;
22683
22759
  if (this.hasMoved) {
22684
22760
  this._core.store.selectionGroup.persistChildren();
22761
+ this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
22685
22762
  this._core.store.selectionGroup.update();
22686
22763
  this._core.engine.emitObjectsChange();
22687
22764
  this._core.store.state.hasObjectsChanged = true;
@@ -23074,6 +23151,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23074
23151
  rotation = 0;
23075
23152
  /** The rotation value of the selection group before the current rotation operation began. */
23076
23153
  initialSelectionGroupRotation = 0;
23154
+ /** The X coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23155
+ initialCenterX = 0;
23156
+ /** The Y coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23157
+ initialCenterY = 0;
23077
23158
  /**
23078
23159
  * Creates an instance of KritzelRotationHandler.
23079
23160
  * @param core - The KritzelCore instance that provides access to the store, engine, and other core functionalities.
@@ -23088,6 +23169,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23088
23169
  reset() {
23089
23170
  this.initialRotation = 0;
23090
23171
  this.rotation = 0;
23172
+ this.initialCenterX = 0;
23173
+ this.initialCenterY = 0;
23091
23174
  }
23092
23175
  /**
23093
23176
  * Handles the pointer down event to initiate a rotation operation.
@@ -23107,6 +23190,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23107
23190
  const objectScale = selectionGroup.scale || 1;
23108
23191
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23109
23192
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23193
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23194
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23195
+ this.initialCenterX = centerX;
23196
+ this.initialCenterY = centerY;
23110
23197
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23111
23198
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23112
23199
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23130,6 +23217,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23130
23217
  const objectScale = selectionGroup.scale || 1;
23131
23218
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23132
23219
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23220
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23221
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23222
+ this.initialCenterX = centerX;
23223
+ this.initialCenterY = centerY;
23133
23224
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23134
23225
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23135
23226
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23154,9 +23245,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23154
23245
  if (this._core.store.state.isRotating && selectionGroup) {
23155
23246
  const clientX = event.clientX - this._core.store.offsetX;
23156
23247
  const clientY = event.clientY - this._core.store.offsetY;
23157
- const objectScale = selectionGroup.scale || 1;
23158
- const groupCenterX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23159
- const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23248
+ const groupCenterX = this.initialCenterX;
23249
+ const groupCenterY = this.initialCenterY;
23160
23250
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23161
23251
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23162
23252
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23174,9 +23264,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23174
23264
  if (this._core.store.state.isRotating && selectionGroup) {
23175
23265
  const clientX = Math.round(firstTouch.clientX - this._core.store.offsetX);
23176
23266
  const clientY = Math.round(firstTouch.clientY - this._core.store.offsetY);
23177
- const objectScale = selectionGroup.scale || 1;
23178
- const groupCenterX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23179
- const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23267
+ const groupCenterX = this.initialCenterX;
23268
+ const groupCenterY = this.initialCenterY;
23180
23269
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23181
23270
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23182
23271
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23855,6 +23944,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23855
23944
  this.hasMoved = false;
23856
23945
  this.currentSnapTarget = null;
23857
23946
  this._core.anchorManager.clearSnapCandidate();
23947
+ this._core.anchorManager.clearSnapPreviewCandidate();
23858
23948
  }
23859
23949
  /**
23860
23950
  * Handles pointer down events to initiate line handle dragging.
@@ -23957,15 +24047,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23957
24047
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23958
24048
  // Get other endpoint's anchor to prevent anchoring both to same object
23959
24049
  const otherAnchorId = line.endAnchor?.objectId;
24050
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
23960
24051
  // Check for snap target
23961
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24052
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
23962
24053
  this.currentSnapTarget = snapTarget;
24054
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
24055
+ let controlWorld = undefined;
24056
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24057
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24058
+ }
23963
24059
  if (snapTarget) {
23964
24060
  // Snap to target center - convert world coords to local
23965
24061
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
23966
24062
  this.updateLineEndpoint(line, localCoords.x, localCoords.y, this.initialEndX, this.initialEndY);
23967
- // Get the other endpoint's world position for edge intersection calculation
23968
- const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
23969
24063
  // Calculate edge intersection point using AnchorManager to support curves
23970
24064
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
23971
24065
  let edgeX;
@@ -23979,30 +24073,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23979
24073
  t = clipInfo.t;
23980
24074
  }
23981
24075
  }
23982
- // Calculate control point in world coordinates if it exists
23983
- let controlWorld = undefined;
23984
- if (line.controlX !== undefined && line.controlY !== undefined) {
23985
- controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
23986
- }
23987
24076
  // Update snap indicator
23988
- this._core.anchorManager.setSnapCandidate({
23989
- objectId: snapTarget.objectId,
23990
- endpoint: 'start',
23991
- centerX: snapTarget.centerX,
23992
- centerY: snapTarget.centerY,
23993
- lineEndpointX: otherEndpointWorld.x,
23994
- lineEndpointY: otherEndpointWorld.y,
23995
- controlX: controlWorld?.x,
23996
- controlY: controlWorld?.y,
23997
- t,
23998
- edgeX,
23999
- edgeY,
24000
- lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24001
- lineStrokeWidth: line.strokeWidth / line.scale,
24002
- arrowOffset: line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined,
24003
- arrowStyle: line.hasStartArrow ? line.arrows?.start?.style : undefined,
24004
- arrowFill: line.hasStartArrow ? line.getArrowFill('start') : undefined,
24005
- });
24077
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24078
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'start', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24079
+ }
24080
+ else if (previewTarget) {
24081
+ const newStartX = this.initialStartX + localDx;
24082
+ const newStartY = this.initialStartY + localDy;
24083
+ this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
24084
+ this._core.anchorManager.clearSnapCandidate();
24085
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'start', previewTarget, otherEndpointWorld, controlWorld));
24006
24086
  }
24007
24087
  else {
24008
24088
  // No snap - use regular position
@@ -24011,6 +24091,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24011
24091
  this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
24012
24092
  // Clear snap indicator
24013
24093
  this._core.anchorManager.clearSnapCandidate();
24094
+ this._core.anchorManager.clearSnapPreviewCandidate();
24014
24095
  }
24015
24096
  }
24016
24097
  else if (handleType === 'end') {
@@ -24019,15 +24100,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24019
24100
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
24020
24101
  // Get other endpoint's anchor to prevent anchoring both to same object
24021
24102
  const otherAnchorId = line.startAnchor?.objectId;
24103
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
24022
24104
  // Check for snap target
24023
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24105
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
24024
24106
  this.currentSnapTarget = snapTarget;
24107
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
24108
+ let controlWorld = undefined;
24109
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24110
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24111
+ }
24025
24112
  if (snapTarget) {
24026
24113
  // Snap to target center - convert world coords to local
24027
24114
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
24028
24115
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, localCoords.x, localCoords.y);
24029
- // Get the other endpoint's world position for edge intersection calculation
24030
- const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
24031
24116
  // Calculate edge intersection point using AnchorManager to support curves
24032
24117
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
24033
24118
  let edgeX;
@@ -24041,30 +24126,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24041
24126
  t = clipInfo.t;
24042
24127
  }
24043
24128
  }
24044
- // Calculate control point in world coordinates if it exists
24045
- let controlWorld = undefined;
24046
- if (line.controlX !== undefined && line.controlY !== undefined) {
24047
- controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24048
- }
24049
24129
  // Update snap indicator
24050
- this._core.anchorManager.setSnapCandidate({
24051
- objectId: snapTarget.objectId,
24052
- endpoint: 'end',
24053
- centerX: snapTarget.centerX,
24054
- centerY: snapTarget.centerY,
24055
- lineEndpointX: otherEndpointWorld.x,
24056
- lineEndpointY: otherEndpointWorld.y,
24057
- controlX: controlWorld?.x,
24058
- controlY: controlWorld?.y,
24059
- t,
24060
- edgeX,
24061
- edgeY,
24062
- lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24063
- lineStrokeWidth: line.strokeWidth / line.scale,
24064
- arrowOffset: line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined,
24065
- arrowStyle: line.hasEndArrow ? line.arrows?.end?.style : undefined,
24066
- arrowFill: line.hasEndArrow ? line.getArrowFill('end') : undefined,
24067
- });
24130
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24131
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'end', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24132
+ }
24133
+ else if (previewTarget) {
24134
+ const newEndX = this.initialEndX + localDx;
24135
+ const newEndY = this.initialEndY + localDy;
24136
+ this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24137
+ this._core.anchorManager.clearSnapCandidate();
24138
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'end', previewTarget, otherEndpointWorld, controlWorld));
24068
24139
  }
24069
24140
  else {
24070
24141
  // No snap - use regular position
@@ -24073,9 +24144,12 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24073
24144
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24074
24145
  // Clear snap indicator
24075
24146
  this._core.anchorManager.clearSnapCandidate();
24147
+ this._core.anchorManager.clearSnapPreviewCandidate();
24076
24148
  }
24077
24149
  }
24078
24150
  else if (handleType === 'center') {
24151
+ this._core.anchorManager.clearSnapPreviewCandidate();
24152
+ this._core.anchorManager.clearSnapCandidate();
24079
24153
  const startControlX = this.initialControlX ?? (this.initialStartX + this.initialEndX) / 2;
24080
24154
  const startControlY = this.initialControlY ?? (this.initialStartY + this.initialEndY) / 2;
24081
24155
  const newControlX = startControlX + localDx * 2;
@@ -24271,6 +24345,28 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24271
24345
  }
24272
24346
  return null;
24273
24347
  }
24348
+ createIndicatorCandidate(line, endpoint, target, otherEndpointWorld, controlWorld, clipInfo) {
24349
+ return {
24350
+ objectId: target.objectId,
24351
+ endpoint,
24352
+ centerX: target.centerX,
24353
+ centerY: target.centerY,
24354
+ lineEndpointX: otherEndpointWorld.x,
24355
+ lineEndpointY: otherEndpointWorld.y,
24356
+ controlX: controlWorld?.x,
24357
+ controlY: controlWorld?.y,
24358
+ t: clipInfo?.t,
24359
+ edgeX: clipInfo?.edgeX,
24360
+ edgeY: clipInfo?.edgeY,
24361
+ lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24362
+ lineStrokeWidth: line.strokeWidth / line.scale,
24363
+ arrowOffset: endpoint === 'start'
24364
+ ? (line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined)
24365
+ : (line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined),
24366
+ arrowStyle: endpoint === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style,
24367
+ arrowFill: endpoint === 'start' ? line.getArrowFill('start') : line.getArrowFill('end'),
24368
+ };
24369
+ }
24274
24370
  }
24275
24371
 
24276
24372
  /**
@@ -27912,6 +28008,14 @@ class KritzelAnchorManager {
27912
28008
  }
27913
28009
  this.snapEndpointToObject(line, entry.endpoint, objectId);
27914
28010
  }
28011
+ const selectionGroup = this._core.store.selectionGroup;
28012
+ if (!selectionGroup || !this._core.store.state.isDragging) {
28013
+ return;
28014
+ }
28015
+ if (selectionGroup.hasExternalAnchoredLineConnections()) {
28016
+ // Keep the local drag overlay aligned with reshaped anchored lines without spamming Yjs.
28017
+ selectionGroup.refreshObjectDimensions(undefined, true);
28018
+ }
27915
28019
  }
27916
28020
  /**
27917
28021
  * Snaps a line endpoint to an object's center position.
@@ -27968,15 +28072,35 @@ class KritzelAnchorManager {
27968
28072
  // Snap Detection
27969
28073
  // ============================================
27970
28074
  /**
27971
- * Finds the nearest object that can be snapped to within the snap threshold.
28075
+ * Finds an object that can be snapped to when the cursor is sufficiently close to its center.
28076
+ * Snap is only triggered when the cursor is inside the object's bounds AND within 1/5 of the
28077
+ * shorter side from the center, so snapping activates only as the user approaches the center
28078
+ * and releases quickly as they move away.
27972
28079
  * Returns null if no suitable snap target is found.
27973
28080
  *
27974
28081
  * @param worldX - X coordinate in world space
27975
28082
  * @param worldY - Y coordinate in world space
27976
28083
  * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
27977
28084
  * @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
28085
+ * @param pointerType - The pointer type driving the drag interaction.
28086
+ */
28087
+ findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, pointerType = 'mouse') {
28088
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, object => {
28089
+ const snapRadius = this.getSnapRadius(object, pointerType);
28090
+ const dx = worldX - object.centerX;
28091
+ const dy = worldY - object.centerY;
28092
+ const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
28093
+ return distanceToCenter <= snapRadius;
28094
+ });
28095
+ }
28096
+ /**
28097
+ * Finds the topmost anchorable object currently containing the dragged endpoint.
28098
+ * Used to drive preview UI before an actual snap engages.
27978
28099
  */
27979
- findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28100
+ findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28101
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
28102
+ }
28103
+ findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
27980
28104
  let bestTarget = null;
27981
28105
  let highestZIndex = -Infinity;
27982
28106
  const objects = this._core.store.allNonSelectionObjects;
@@ -27996,20 +28120,30 @@ class KritzelAnchorManager {
27996
28120
  // Check if point is inside the object's rotated polygon
27997
28121
  const polygon = object.rotatedPolygon;
27998
28122
  const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
27999
- if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
28000
- // If inside, check if this object is "above" the current best target
28001
- if (object.zIndex > highestZIndex) {
28002
- highestZIndex = object.zIndex;
28003
- bestTarget = {
28004
- objectId: object.id,
28005
- centerX: object.centerX,
28006
- centerY: object.centerY,
28007
- };
28008
- }
28123
+ if (!KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
28124
+ continue;
28125
+ }
28126
+ if (!predicate(object)) {
28127
+ continue;
28128
+ }
28129
+ if (object.zIndex > highestZIndex) {
28130
+ highestZIndex = object.zIndex;
28131
+ bestTarget = {
28132
+ objectId: object.id,
28133
+ centerX: object.centerX,
28134
+ centerY: object.centerY,
28135
+ };
28009
28136
  }
28010
28137
  }
28011
28138
  return bestTarget;
28012
28139
  }
28140
+ getSnapRadius(object, pointerType) {
28141
+ const baseRadius = Math.min(object.width, object.height) / 5;
28142
+ if (pointerType === 'touch' || pointerType === 'pen') {
28143
+ return Math.max(baseRadius * 1.75, 24 / this.getSafeScale(this._core.store.state.scale));
28144
+ }
28145
+ return baseRadius;
28146
+ }
28013
28147
  /**
28014
28148
  * Sets the current snap candidate for visual feedback during line endpoint dragging.
28015
28149
  * Updates the store state and triggers a rerender to show the snap indicator.
@@ -28020,6 +28154,17 @@ class KritzelAnchorManager {
28020
28154
  this._core.store.state.snapCandidate = candidate;
28021
28155
  this._core.rerender();
28022
28156
  }
28157
+ /**
28158
+ * Sets the current snap preview candidate for early visual feedback during line dragging.
28159
+ * Preview state must remain separate from active snap state so line clipping and anchor
28160
+ * commit semantics only react to actual snaps.
28161
+ *
28162
+ * @param candidate - The preview candidate object, or null to clear.
28163
+ */
28164
+ setSnapPreviewCandidate(candidate) {
28165
+ this._core.store.state.snapPreviewCandidate = candidate;
28166
+ this._core.rerender();
28167
+ }
28023
28168
  /**
28024
28169
  * Gets the current snap candidate from the store state.
28025
28170
  *
@@ -28028,6 +28173,14 @@ class KritzelAnchorManager {
28028
28173
  getSnapCandidate() {
28029
28174
  return this._core.store.state.snapCandidate ?? null;
28030
28175
  }
28176
+ /**
28177
+ * Gets the current snap preview candidate from the store state.
28178
+ *
28179
+ * @returns The current preview candidate if one exists, null otherwise.
28180
+ */
28181
+ getSnapPreviewCandidate() {
28182
+ return this._core.store.state.snapPreviewCandidate ?? null;
28183
+ }
28031
28184
  /**
28032
28185
  * Clears the snap candidate from the store state and triggers a rerender.
28033
28186
  * Should be called when snapping is cancelled or completed.
@@ -28036,6 +28189,13 @@ class KritzelAnchorManager {
28036
28189
  this._core.store.state.snapCandidate = null;
28037
28190
  this._core.rerender();
28038
28191
  }
28192
+ /**
28193
+ * Clears the snap preview candidate from the store state and triggers a rerender.
28194
+ */
28195
+ clearSnapPreviewCandidate() {
28196
+ this._core.store.state.snapPreviewCandidate = null;
28197
+ this._core.rerender();
28198
+ }
28039
28199
  // ============================================
28040
28200
  // Render Data
28041
28201
  // ============================================
@@ -28089,33 +28249,43 @@ class KritzelAnchorManager {
28089
28249
  * or null if there's no active snap candidate.
28090
28250
  */
28091
28251
  getSnapIndicatorRenderData() {
28092
- const snapCandidate = this.getSnapCandidate();
28093
- if (!snapCandidate)
28252
+ return this.getIndicatorRenderDataForCandidate(this.getSnapCandidate());
28253
+ }
28254
+ /**
28255
+ * Gets render data for snap preview visualization.
28256
+ *
28257
+ * @returns SnapIndicatorRenderData for the active preview candidate, or null if none exists.
28258
+ */
28259
+ getSnapPreviewRenderData() {
28260
+ return this.getIndicatorRenderDataForCandidate(this.getSnapPreviewCandidate());
28261
+ }
28262
+ getIndicatorRenderDataForCandidate(candidate) {
28263
+ if (!candidate)
28094
28264
  return null;
28095
- if (!this.areFiniteNumbers(snapCandidate.centerX, snapCandidate.centerY, snapCandidate.lineEndpointX, snapCandidate.lineEndpointY)) {
28265
+ if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
28096
28266
  return null;
28097
28267
  }
28098
28268
  const scale = this.getSafeScale(this._core.store.state.scale);
28099
28269
  const indicatorRadius = 8 / scale;
28100
28270
  const indicatorStrokeWidth = `${2 / scale}`;
28101
- const lineStrokeWidthNum = this.areFiniteNumbers(snapCandidate.lineStrokeWidth) && snapCandidate.lineStrokeWidth > 0
28102
- ? snapCandidate.lineStrokeWidth
28271
+ const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
28272
+ ? candidate.lineStrokeWidth
28103
28273
  : (4 / scale);
28104
28274
  const lineStrokeWidth = `${lineStrokeWidthNum}`;
28105
28275
  const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
28106
28276
  const dashArray = `${dashLength} ${dashLength}`;
28107
- const lineStroke = snapCandidate.lineStroke || '#000000';
28108
- let edgeX = this.areFiniteNumbers(snapCandidate.edgeX) ? snapCandidate.edgeX : undefined;
28109
- let edgeY = this.areFiniteNumbers(snapCandidate.edgeY) ? snapCandidate.edgeY : undefined;
28277
+ const lineStroke = candidate.lineStroke || '#000000';
28278
+ let edgeX = this.areFiniteNumbers(candidate.edgeX) ? candidate.edgeX : undefined;
28279
+ let edgeY = this.areFiniteNumbers(candidate.edgeY) ? candidate.edgeY : undefined;
28110
28280
  let solidLineEndX = edgeX;
28111
28281
  let solidLineEndY = edgeY;
28112
28282
  let arrowPoints;
28113
- const arrowOffset = this.areFiniteNumbers(snapCandidate.arrowOffset) && snapCandidate.arrowOffset > 0
28114
- ? snapCandidate.arrowOffset
28283
+ const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
28284
+ ? candidate.arrowOffset
28115
28285
  : undefined;
28116
28286
  if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
28117
- const dx = snapCandidate.lineEndpointX - edgeX;
28118
- const dy = snapCandidate.lineEndpointY - edgeY;
28287
+ const dx = candidate.lineEndpointX - edgeX;
28288
+ const dy = candidate.lineEndpointY - edgeY;
28119
28289
  const length = Math.sqrt(dx * dx + dy * dy);
28120
28290
  if (length > arrowOffset) {
28121
28291
  solidLineEndX = edgeX + (dx / length) * arrowOffset;
@@ -28123,8 +28293,8 @@ class KritzelAnchorManager {
28123
28293
  }
28124
28294
  // Calculate arrow head points
28125
28295
  // Direction from line endpoint to edge (arrow direction)
28126
- const arrowDx = edgeX - snapCandidate.lineEndpointX;
28127
- const arrowDy = edgeY - snapCandidate.lineEndpointY;
28296
+ const arrowDx = edgeX - candidate.lineEndpointX;
28297
+ const arrowDy = edgeY - candidate.lineEndpointY;
28128
28298
  const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
28129
28299
  if (arrowLengthTotal > 0) {
28130
28300
  const ux = arrowDx / arrowLengthTotal;
@@ -28155,15 +28325,15 @@ class KritzelAnchorManager {
28155
28325
  return null;
28156
28326
  }
28157
28327
  const snapLinePath = (() => {
28158
- if (snapCandidate.controlX !== undefined &&
28159
- snapCandidate.controlY !== undefined &&
28160
- snapCandidate.t !== undefined &&
28161
- this.areFiniteNumbers(snapCandidate.controlX, snapCandidate.controlY, snapCandidate.t)) {
28162
- const startT = snapCandidate.endpoint === 'start' ? 1 - snapCandidate.t : snapCandidate.t;
28328
+ if (candidate.controlX !== undefined &&
28329
+ candidate.controlY !== undefined &&
28330
+ candidate.t !== undefined &&
28331
+ this.areFiniteNumbers(candidate.controlX, candidate.controlY, candidate.t)) {
28332
+ const startT = candidate.endpoint === 'start' ? 1 - candidate.t : candidate.t;
28163
28333
  // Ensure meaningful range
28164
28334
  if (startT >= 1)
28165
28335
  return undefined;
28166
- const segment = this.extractQuadraticSegment({ x: snapCandidate.lineEndpointX, y: snapCandidate.lineEndpointY }, { x: snapCandidate.controlX, y: snapCandidate.controlY }, { x: snapCandidate.centerX, y: snapCandidate.centerY }, startT, 1);
28336
+ const segment = this.extractQuadraticSegment({ x: candidate.lineEndpointX, y: candidate.lineEndpointY }, { x: candidate.controlX, y: candidate.controlY }, { x: candidate.centerX, y: candidate.centerY }, startT, 1);
28167
28337
  if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
28168
28338
  return undefined;
28169
28339
  }
@@ -28177,15 +28347,15 @@ class KritzelAnchorManager {
28177
28347
  lineStrokeWidth,
28178
28348
  dashArray,
28179
28349
  lineStroke,
28180
- centerX: snapCandidate.centerX,
28181
- centerY: snapCandidate.centerY,
28182
- lineEndpointX: snapCandidate.lineEndpointX,
28183
- lineEndpointY: snapCandidate.lineEndpointY,
28350
+ centerX: candidate.centerX,
28351
+ centerY: candidate.centerY,
28352
+ lineEndpointX: candidate.lineEndpointX,
28353
+ lineEndpointY: candidate.lineEndpointY,
28184
28354
  edgeX,
28185
28355
  edgeY,
28186
28356
  arrowOffset,
28187
- arrowStyle: snapCandidate.arrowStyle,
28188
- arrowFill: snapCandidate.arrowFill,
28357
+ arrowStyle: candidate.arrowStyle,
28358
+ arrowFill: candidate.arrowFill,
28189
28359
  solidLineEndX,
28190
28360
  solidLineEndY,
28191
28361
  arrowPoints,