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.
Files changed (41) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_45.cjs.entry.js +13 -8
  3. package/dist/cjs/{schema.constants-Np7j2Gz4.js → schema.constants-9v4Edzoe.js} +289 -108
  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/resize.handler.js +22 -0
  7. package/dist/collection/classes/handlers/rotation.handler.js +18 -6
  8. package/dist/collection/classes/managers/anchor.manager.js +108 -45
  9. package/dist/collection/classes/objects/selection-group.class.js +80 -5
  10. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +10 -6
  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-B5XGjTLd.js → p-BmKYyPpl.js} +1 -1
  20. package/dist/components/{p-DNDt6O6A.js → p-CRKezkZU.js} +1 -1
  21. package/dist/components/{p-Dw-t1qQc.js → p-DXYkgNAO.js} +1 -1
  22. package/dist/components/{p-DF0S4FqL.js → p-G3xSPZ0x.js} +1 -1
  23. package/dist/components/p-ioK7ttPL.js +9 -0
  24. package/dist/esm/index.js +2 -2
  25. package/dist/esm/kritzel-active-users_45.entry.js +13 -8
  26. package/dist/esm/{schema.constants-1SDhlkfS.js → schema.constants-BT66X_r5.js} +289 -108
  27. package/dist/stencil/index.esm.js +1 -1
  28. package/dist/stencil/p-9ca04088.entry.js +9 -0
  29. package/dist/stencil/{p-1SDhlkfS.js → p-BT66X_r5.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/resize.handler.d.ts +7 -0
  33. package/dist/types/classes/handlers/rotation.handler.d.ts +4 -0
  34. package/dist/types/classes/managers/anchor.manager.d.ts +35 -2
  35. package/dist/types/classes/objects/selection-group.class.d.ts +8 -1
  36. package/dist/types/constants/version.d.ts +1 -1
  37. package/dist/types/interfaces/anchor.interface.d.ts +12 -2
  38. package/dist/types/interfaces/engine-state.interface.d.ts +2 -1
  39. package/package.json +1 -1
  40. package/dist/components/p-6_95PFq4.js +0 -9
  41. package/dist/stencil/p-7ba7df62.entry.js +0 -9
@@ -20682,6 +20682,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
20682
20682
  _lastChildPersistTime = 0;
20683
20683
  /** Minimum interval (ms) between child Yjs persists during drag */
20684
20684
  static CHILD_PERSIST_THROTTLE_MS = 100;
20685
+ /** Ignore tiny floating-point diffs when comparing refreshed bounds */
20686
+ static DIMENSION_EPSILON = 0.001;
20685
20687
  /**
20686
20688
  * Gets the array of object IDs contained in this selection group.
20687
20689
  * @returns Array of string IDs for the selected objects
@@ -21055,6 +21057,30 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21055
21057
  }
21056
21058
  });
21057
21059
  }
21060
+ /**
21061
+ * Returns true when the selection contains a line anchored to an object
21062
+ * that is not part of this selection group.
21063
+ */
21064
+ hasExternalAnchoredLineConnections(cachedObjects) {
21065
+ const children = cachedObjects ?? this.objects;
21066
+ if (children.length === 0) {
21067
+ return false;
21068
+ }
21069
+ const selectedIds = new Set(children.map(child => child.id));
21070
+ for (const child of children) {
21071
+ if (!KritzelClassHelper.isInstanceOf(child, 'KritzelLine')) {
21072
+ continue;
21073
+ }
21074
+ const line = child;
21075
+ if (line.startAnchor && !selectedIds.has(line.startAnchor.objectId)) {
21076
+ return true;
21077
+ }
21078
+ if (line.endAnchor && !selectedIds.has(line.endAnchor.objectId)) {
21079
+ return true;
21080
+ }
21081
+ }
21082
+ return false;
21083
+ }
21058
21084
  /**
21059
21085
  * Resizes the selection group and scales all contained objects proportionally.
21060
21086
  * Uses snapshot values to avoid compounding errors during continuous drag operations.
@@ -21147,8 +21173,13 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21147
21173
  */
21148
21174
  rotate(value) {
21149
21175
  this.rotation = value;
21150
- const centerX = this.translateX + this.totalWidth / 2 / this.scale;
21151
- const centerY = this.translateY + this.totalHeight / 2 / this.scale;
21176
+ // Use the snapshot center as the rotation pivot rather than the live center.
21177
+ // refreshObjectDimensions() below can grow the box when an anchored line is pinned
21178
+ // to an external object; keeping the pivot fixed prevents the children from drifting.
21179
+ const snapshotTotalWidth = this.snapshotWidth + this.padding * 2;
21180
+ const snapshotTotalHeight = this.snapshotHeight + this.padding * 2;
21181
+ const centerX = this.snapshotTranslateX + snapshotTotalWidth / 2 / this.scale;
21182
+ const centerY = this.snapshotTranslateY + snapshotTotalHeight / 2 / this.scale;
21152
21183
  const angle = value - this.snapshotRotation;
21153
21184
  const cos = Math.cos(angle);
21154
21185
  const sin = Math.sin(angle);
@@ -21170,6 +21201,31 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21170
21201
  child.translateY = centerY + rotatedY - child.totalHeight / 2 / child.scale;
21171
21202
  child.rotate(childCount === 1 ? value : unchangedSnapshot.rotation + angle);
21172
21203
  }
21204
+ // Re-sync any anchored lines so their stored geometry follows the rotated objects
21205
+ // (including lines anchored to an object outside this selection), then grow the
21206
+ // selection box to enclose the reshaped lines. Rotation sets state.isRotating
21207
+ // rather than state.isDragging, so the refresh must be triggered explicitly here.
21208
+ const anchorTargetIds = new Set();
21209
+ for (const child of children) {
21210
+ anchorTargetIds.add(child.id);
21211
+ if (KritzelClassHelper.isInstanceOf(child, 'KritzelLine')) {
21212
+ const line = child;
21213
+ if (line.startAnchor)
21214
+ anchorTargetIds.add(line.startAnchor.objectId);
21215
+ if (line.endAnchor)
21216
+ anchorTargetIds.add(line.endAnchor.objectId);
21217
+ }
21218
+ }
21219
+ let hasAnchoredLines = false;
21220
+ for (const targetId of anchorTargetIds) {
21221
+ if (this._core.anchorManager.getLinesAnchoredTo(targetId).length > 0) {
21222
+ hasAnchoredLines = true;
21223
+ this._core.anchorManager.updateAnchorsForObject(targetId);
21224
+ }
21225
+ }
21226
+ if (hasAnchoredLines) {
21227
+ this.refreshObjectDimensions(children);
21228
+ }
21173
21229
  });
21174
21230
  }
21175
21231
  /**
@@ -21199,6 +21255,16 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21199
21255
  * @param skipPersist If true, only updates local state without persisting to Yjs (used during undo/redo)
21200
21256
  */
21201
21257
  refreshObjectDimensions(cachedObjects, skipPersist = false) {
21258
+ const previous = {
21259
+ minX: this.minX,
21260
+ minY: this.minY,
21261
+ maxX: this.maxX,
21262
+ maxY: this.maxY,
21263
+ width: this.width,
21264
+ height: this.height,
21265
+ translateX: this.translateX,
21266
+ translateY: this.translateY,
21267
+ };
21202
21268
  const children = cachedObjects ?? this.objects;
21203
21269
  if (children.length === 1) {
21204
21270
  const obj = children[0];
@@ -21252,9 +21318,18 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21252
21318
  this.translateX = cx - (this.width / this.scale + 2 * this.padding) / 2;
21253
21319
  this.translateY = cy - (this.height / this.scale + 2 * this.padding) / 2;
21254
21320
  }
21255
- if (!skipPersist) {
21321
+ const hasChanged = Math.abs(this.minX - previous.minX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21322
+ Math.abs(this.minY - previous.minY) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21323
+ Math.abs(this.maxX - previous.maxX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21324
+ Math.abs(this.maxY - previous.maxY) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21325
+ Math.abs(this.width - previous.width) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21326
+ Math.abs(this.height - previous.height) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21327
+ Math.abs(this.translateX - previous.translateX) > KritzelSelectionGroup.DIMENSION_EPSILON ||
21328
+ Math.abs(this.translateY - previous.translateY) > KritzelSelectionGroup.DIMENSION_EPSILON;
21329
+ if (!skipPersist && hasChanged) {
21256
21330
  this._core.store.objects.update(this);
21257
21331
  }
21332
+ return hasChanged;
21258
21333
  }
21259
21334
  /**
21260
21335
  * Calculates the horizontal offset from the group's center to an object's center using snapshot data.
@@ -21264,7 +21339,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21264
21339
  */
21265
21340
  getOffsetXToCenterFromSnapshot(snapshot) {
21266
21341
  const objCenterX = snapshot.translateX + snapshot.totalWidth / snapshot.scale / 2;
21267
- const groupCenterX = this.translateX + this.totalWidth / this.scale / 2;
21342
+ const groupCenterX = this.snapshotTranslateX + (this.snapshotWidth + this.padding * 2) / this.scale / 2;
21268
21343
  return objCenterX - groupCenterX;
21269
21344
  }
21270
21345
  /**
@@ -21275,7 +21350,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
21275
21350
  */
21276
21351
  getOffsetYToCenterFromSnapshot(snapshot) {
21277
21352
  const objCenterY = snapshot.translateY + snapshot.totalHeight / snapshot.scale / 2;
21278
- const groupCenterY = this.translateY + this.totalHeight / this.scale / 2;
21353
+ const groupCenterY = this.snapshotTranslateY + (this.snapshotHeight + this.padding * 2) / this.scale / 2;
21279
21354
  return objCenterY - groupCenterY;
21280
21355
  }
21281
21356
  /**
@@ -22648,6 +22723,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
22648
22723
  this._core.store.state.isDragging = false;
22649
22724
  if (this.hasMoved) {
22650
22725
  this._core.store.selectionGroup.persistChildren();
22726
+ this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
22651
22727
  this._core.store.selectionGroup.update();
22652
22728
  this._core.engine.emitObjectsChange();
22653
22729
  this._core.store.state.hasObjectsChanged = true;
@@ -22660,6 +22736,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
22660
22736
  this._core.store.state.isDragging = false;
22661
22737
  if (this.hasMoved) {
22662
22738
  this._core.store.selectionGroup.persistChildren();
22739
+ this._core.store.selectionGroup.refreshObjectDimensions(undefined, false);
22663
22740
  this._core.store.selectionGroup.update();
22664
22741
  this._core.engine.emitObjectsChange();
22665
22742
  this._core.store.state.hasObjectsChanged = true;
@@ -22772,6 +22849,22 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22772
22849
  this.newSize = { x: 0, y: 0, width: 0, height: 0 };
22773
22850
  this.hasResized = false;
22774
22851
  }
22852
+ /**
22853
+ * Rebase pointer and size baselines to the latest live selection bounds.
22854
+ * This prevents drift when anchored lines to external objects expand/shrink
22855
+ * the selection box while a resize gesture is in progress.
22856
+ */
22857
+ rebaseResizeBaseline(selectionGroup, clientX, clientY) {
22858
+ this.initialMouseX = clientX;
22859
+ this.initialMouseY = clientY;
22860
+ this.initialSize.width = selectionGroup.width;
22861
+ this.initialSize.height = selectionGroup.height;
22862
+ this.initialSize.x = selectionGroup.translateX;
22863
+ this.initialSize.y = selectionGroup.translateY;
22864
+ }
22865
+ shouldRebaseResizeBaseline(selectionGroup) {
22866
+ return selectionGroup.hasExternalAnchoredLineConnections?.() === true;
22867
+ }
22775
22868
  /**
22776
22869
  * Handles the pointer down event to initiate a resize operation.
22777
22870
  * Captures the initial mouse position and selection group dimensions when
@@ -22899,6 +22992,9 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22899
22992
  this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
22900
22993
  this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
22901
22994
  selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
22995
+ if (this.shouldRebaseResizeBaseline(selectionGroup)) {
22996
+ this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
22997
+ }
22902
22998
  }
22903
22999
  }
22904
23000
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
@@ -22965,6 +23061,9 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22965
23061
  this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
22966
23062
  this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
22967
23063
  selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
23064
+ if (this.shouldRebaseResizeBaseline(selectionGroup)) {
23065
+ this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
23066
+ }
22968
23067
  }
22969
23068
  }
22970
23069
  }
@@ -23052,6 +23151,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23052
23151
  rotation = 0;
23053
23152
  /** The rotation value of the selection group before the current rotation operation began. */
23054
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;
23055
23158
  /**
23056
23159
  * Creates an instance of KritzelRotationHandler.
23057
23160
  * @param core - The KritzelCore instance that provides access to the store, engine, and other core functionalities.
@@ -23066,6 +23169,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23066
23169
  reset() {
23067
23170
  this.initialRotation = 0;
23068
23171
  this.rotation = 0;
23172
+ this.initialCenterX = 0;
23173
+ this.initialCenterY = 0;
23069
23174
  }
23070
23175
  /**
23071
23176
  * Handles the pointer down event to initiate a rotation operation.
@@ -23085,6 +23190,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23085
23190
  const objectScale = selectionGroup.scale || 1;
23086
23191
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23087
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;
23088
23197
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23089
23198
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23090
23199
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23108,6 +23217,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23108
23217
  const objectScale = selectionGroup.scale || 1;
23109
23218
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23110
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;
23111
23224
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23112
23225
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23113
23226
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23132,9 +23245,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23132
23245
  if (this._core.store.state.isRotating && selectionGroup) {
23133
23246
  const clientX = event.clientX - this._core.store.offsetX;
23134
23247
  const clientY = event.clientY - this._core.store.offsetY;
23135
- const objectScale = selectionGroup.scale || 1;
23136
- const groupCenterX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23137
- const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23248
+ const groupCenterX = this.initialCenterX;
23249
+ const groupCenterY = this.initialCenterY;
23138
23250
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23139
23251
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23140
23252
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23152,9 +23264,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23152
23264
  if (this._core.store.state.isRotating && selectionGroup) {
23153
23265
  const clientX = Math.round(firstTouch.clientX - this._core.store.offsetX);
23154
23266
  const clientY = Math.round(firstTouch.clientY - this._core.store.offsetY);
23155
- const objectScale = selectionGroup.scale || 1;
23156
- const groupCenterX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23157
- const groupCenterY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23267
+ const groupCenterX = this.initialCenterX;
23268
+ const groupCenterY = this.initialCenterY;
23158
23269
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23159
23270
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23160
23271
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23833,6 +23944,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23833
23944
  this.hasMoved = false;
23834
23945
  this.currentSnapTarget = null;
23835
23946
  this._core.anchorManager.clearSnapCandidate();
23947
+ this._core.anchorManager.clearSnapPreviewCandidate();
23836
23948
  }
23837
23949
  /**
23838
23950
  * Handles pointer down events to initiate line handle dragging.
@@ -23935,15 +24047,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23935
24047
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23936
24048
  // Get other endpoint's anchor to prevent anchoring both to same object
23937
24049
  const otherAnchorId = line.endAnchor?.objectId;
24050
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
23938
24051
  // Check for snap target
23939
- 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);
23940
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
+ }
23941
24059
  if (snapTarget) {
23942
24060
  // Snap to target center - convert world coords to local
23943
24061
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
23944
24062
  this.updateLineEndpoint(line, localCoords.x, localCoords.y, this.initialEndX, this.initialEndY);
23945
- // Get the other endpoint's world position for edge intersection calculation
23946
- const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
23947
24063
  // Calculate edge intersection point using AnchorManager to support curves
23948
24064
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
23949
24065
  let edgeX;
@@ -23957,30 +24073,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23957
24073
  t = clipInfo.t;
23958
24074
  }
23959
24075
  }
23960
- // Calculate control point in world coordinates if it exists
23961
- let controlWorld = undefined;
23962
- if (line.controlX !== undefined && line.controlY !== undefined) {
23963
- controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
23964
- }
23965
24076
  // Update snap indicator
23966
- this._core.anchorManager.setSnapCandidate({
23967
- objectId: snapTarget.objectId,
23968
- endpoint: 'start',
23969
- centerX: snapTarget.centerX,
23970
- centerY: snapTarget.centerY,
23971
- lineEndpointX: otherEndpointWorld.x,
23972
- lineEndpointY: otherEndpointWorld.y,
23973
- controlX: controlWorld?.x,
23974
- controlY: controlWorld?.y,
23975
- t,
23976
- edgeX,
23977
- edgeY,
23978
- lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
23979
- lineStrokeWidth: line.strokeWidth / line.scale,
23980
- arrowOffset: line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined,
23981
- arrowStyle: line.hasStartArrow ? line.arrows?.start?.style : undefined,
23982
- arrowFill: line.hasStartArrow ? line.getArrowFill('start') : undefined,
23983
- });
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));
23984
24086
  }
23985
24087
  else {
23986
24088
  // No snap - use regular position
@@ -23989,6 +24091,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23989
24091
  this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
23990
24092
  // Clear snap indicator
23991
24093
  this._core.anchorManager.clearSnapCandidate();
24094
+ this._core.anchorManager.clearSnapPreviewCandidate();
23992
24095
  }
23993
24096
  }
23994
24097
  else if (handleType === 'end') {
@@ -23997,15 +24100,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23997
24100
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23998
24101
  // Get other endpoint's anchor to prevent anchoring both to same object
23999
24102
  const otherAnchorId = line.startAnchor?.objectId;
24103
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
24000
24104
  // Check for snap target
24001
- 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);
24002
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
+ }
24003
24112
  if (snapTarget) {
24004
24113
  // Snap to target center - convert world coords to local
24005
24114
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
24006
24115
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, localCoords.x, localCoords.y);
24007
- // Get the other endpoint's world position for edge intersection calculation
24008
- const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
24009
24116
  // Calculate edge intersection point using AnchorManager to support curves
24010
24117
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
24011
24118
  let edgeX;
@@ -24019,30 +24126,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24019
24126
  t = clipInfo.t;
24020
24127
  }
24021
24128
  }
24022
- // Calculate control point in world coordinates if it exists
24023
- let controlWorld = undefined;
24024
- if (line.controlX !== undefined && line.controlY !== undefined) {
24025
- controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24026
- }
24027
24129
  // Update snap indicator
24028
- this._core.anchorManager.setSnapCandidate({
24029
- objectId: snapTarget.objectId,
24030
- endpoint: 'end',
24031
- centerX: snapTarget.centerX,
24032
- centerY: snapTarget.centerY,
24033
- lineEndpointX: otherEndpointWorld.x,
24034
- lineEndpointY: otherEndpointWorld.y,
24035
- controlX: controlWorld?.x,
24036
- controlY: controlWorld?.y,
24037
- t,
24038
- edgeX,
24039
- edgeY,
24040
- lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24041
- lineStrokeWidth: line.strokeWidth / line.scale,
24042
- arrowOffset: line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined,
24043
- arrowStyle: line.hasEndArrow ? line.arrows?.end?.style : undefined,
24044
- arrowFill: line.hasEndArrow ? line.getArrowFill('end') : undefined,
24045
- });
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));
24046
24139
  }
24047
24140
  else {
24048
24141
  // No snap - use regular position
@@ -24051,9 +24144,12 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24051
24144
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24052
24145
  // Clear snap indicator
24053
24146
  this._core.anchorManager.clearSnapCandidate();
24147
+ this._core.anchorManager.clearSnapPreviewCandidate();
24054
24148
  }
24055
24149
  }
24056
24150
  else if (handleType === 'center') {
24151
+ this._core.anchorManager.clearSnapPreviewCandidate();
24152
+ this._core.anchorManager.clearSnapCandidate();
24057
24153
  const startControlX = this.initialControlX ?? (this.initialStartX + this.initialEndX) / 2;
24058
24154
  const startControlY = this.initialControlY ?? (this.initialStartY + this.initialEndY) / 2;
24059
24155
  const newControlX = startControlX + localDx * 2;
@@ -24249,6 +24345,28 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24249
24345
  }
24250
24346
  return null;
24251
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
+ }
24252
24370
  }
24253
24371
 
24254
24372
  /**
@@ -27890,6 +28008,14 @@ class KritzelAnchorManager {
27890
28008
  }
27891
28009
  this.snapEndpointToObject(line, entry.endpoint, objectId);
27892
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
+ }
27893
28019
  }
27894
28020
  /**
27895
28021
  * Snaps a line endpoint to an object's center position.
@@ -27956,8 +28082,25 @@ class KritzelAnchorManager {
27956
28082
  * @param worldY - Y coordinate in world space
27957
28083
  * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
27958
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.
27959
28099
  */
27960
- 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) {
27961
28104
  let bestTarget = null;
27962
28105
  let highestZIndex = -Infinity;
27963
28106
  const objects = this._core.store.allNonSelectionObjects;
@@ -27977,28 +28120,30 @@ class KritzelAnchorManager {
27977
28120
  // Check if point is inside the object's rotated polygon
27978
28121
  const polygon = object.rotatedPolygon;
27979
28122
  const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
27980
- if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
27981
- // Only snap when the cursor is near the object's center (within 1/5 of the shorter side)
27982
- const snapRadius = Math.min(object.width, object.height) / 5;
27983
- const dx = worldX - object.centerX;
27984
- const dy = worldY - object.centerY;
27985
- const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
27986
- if (distanceToCenter > snapRadius) {
27987
- continue;
27988
- }
27989
- // If inside and close enough to center, check if this object is "above" the current best target
27990
- if (object.zIndex > highestZIndex) {
27991
- highestZIndex = object.zIndex;
27992
- bestTarget = {
27993
- objectId: object.id,
27994
- centerX: object.centerX,
27995
- centerY: object.centerY,
27996
- };
27997
- }
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
+ };
27998
28136
  }
27999
28137
  }
28000
28138
  return bestTarget;
28001
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
+ }
28002
28147
  /**
28003
28148
  * Sets the current snap candidate for visual feedback during line endpoint dragging.
28004
28149
  * Updates the store state and triggers a rerender to show the snap indicator.
@@ -28009,6 +28154,17 @@ class KritzelAnchorManager {
28009
28154
  this._core.store.state.snapCandidate = candidate;
28010
28155
  this._core.rerender();
28011
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
+ }
28012
28168
  /**
28013
28169
  * Gets the current snap candidate from the store state.
28014
28170
  *
@@ -28017,6 +28173,14 @@ class KritzelAnchorManager {
28017
28173
  getSnapCandidate() {
28018
28174
  return this._core.store.state.snapCandidate ?? null;
28019
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
+ }
28020
28184
  /**
28021
28185
  * Clears the snap candidate from the store state and triggers a rerender.
28022
28186
  * Should be called when snapping is cancelled or completed.
@@ -28025,6 +28189,13 @@ class KritzelAnchorManager {
28025
28189
  this._core.store.state.snapCandidate = null;
28026
28190
  this._core.rerender();
28027
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
+ }
28028
28199
  // ============================================
28029
28200
  // Render Data
28030
28201
  // ============================================
@@ -28078,33 +28249,43 @@ class KritzelAnchorManager {
28078
28249
  * or null if there's no active snap candidate.
28079
28250
  */
28080
28251
  getSnapIndicatorRenderData() {
28081
- const snapCandidate = this.getSnapCandidate();
28082
- 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)
28083
28264
  return null;
28084
- if (!this.areFiniteNumbers(snapCandidate.centerX, snapCandidate.centerY, snapCandidate.lineEndpointX, snapCandidate.lineEndpointY)) {
28265
+ if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
28085
28266
  return null;
28086
28267
  }
28087
28268
  const scale = this.getSafeScale(this._core.store.state.scale);
28088
28269
  const indicatorRadius = 8 / scale;
28089
28270
  const indicatorStrokeWidth = `${2 / scale}`;
28090
- const lineStrokeWidthNum = this.areFiniteNumbers(snapCandidate.lineStrokeWidth) && snapCandidate.lineStrokeWidth > 0
28091
- ? snapCandidate.lineStrokeWidth
28271
+ const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
28272
+ ? candidate.lineStrokeWidth
28092
28273
  : (4 / scale);
28093
28274
  const lineStrokeWidth = `${lineStrokeWidthNum}`;
28094
28275
  const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
28095
28276
  const dashArray = `${dashLength} ${dashLength}`;
28096
- const lineStroke = snapCandidate.lineStroke || '#000000';
28097
- let edgeX = this.areFiniteNumbers(snapCandidate.edgeX) ? snapCandidate.edgeX : undefined;
28098
- 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;
28099
28280
  let solidLineEndX = edgeX;
28100
28281
  let solidLineEndY = edgeY;
28101
28282
  let arrowPoints;
28102
- const arrowOffset = this.areFiniteNumbers(snapCandidate.arrowOffset) && snapCandidate.arrowOffset > 0
28103
- ? snapCandidate.arrowOffset
28283
+ const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
28284
+ ? candidate.arrowOffset
28104
28285
  : undefined;
28105
28286
  if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
28106
- const dx = snapCandidate.lineEndpointX - edgeX;
28107
- const dy = snapCandidate.lineEndpointY - edgeY;
28287
+ const dx = candidate.lineEndpointX - edgeX;
28288
+ const dy = candidate.lineEndpointY - edgeY;
28108
28289
  const length = Math.sqrt(dx * dx + dy * dy);
28109
28290
  if (length > arrowOffset) {
28110
28291
  solidLineEndX = edgeX + (dx / length) * arrowOffset;
@@ -28112,8 +28293,8 @@ class KritzelAnchorManager {
28112
28293
  }
28113
28294
  // Calculate arrow head points
28114
28295
  // Direction from line endpoint to edge (arrow direction)
28115
- const arrowDx = edgeX - snapCandidate.lineEndpointX;
28116
- const arrowDy = edgeY - snapCandidate.lineEndpointY;
28296
+ const arrowDx = edgeX - candidate.lineEndpointX;
28297
+ const arrowDy = edgeY - candidate.lineEndpointY;
28117
28298
  const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
28118
28299
  if (arrowLengthTotal > 0) {
28119
28300
  const ux = arrowDx / arrowLengthTotal;
@@ -28144,15 +28325,15 @@ class KritzelAnchorManager {
28144
28325
  return null;
28145
28326
  }
28146
28327
  const snapLinePath = (() => {
28147
- if (snapCandidate.controlX !== undefined &&
28148
- snapCandidate.controlY !== undefined &&
28149
- snapCandidate.t !== undefined &&
28150
- this.areFiniteNumbers(snapCandidate.controlX, snapCandidate.controlY, snapCandidate.t)) {
28151
- 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;
28152
28333
  // Ensure meaningful range
28153
28334
  if (startT >= 1)
28154
28335
  return undefined;
28155
- 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);
28156
28337
  if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
28157
28338
  return undefined;
28158
28339
  }
@@ -28166,15 +28347,15 @@ class KritzelAnchorManager {
28166
28347
  lineStrokeWidth,
28167
28348
  dashArray,
28168
28349
  lineStroke,
28169
- centerX: snapCandidate.centerX,
28170
- centerY: snapCandidate.centerY,
28171
- lineEndpointX: snapCandidate.lineEndpointX,
28172
- lineEndpointY: snapCandidate.lineEndpointY,
28350
+ centerX: candidate.centerX,
28351
+ centerY: candidate.centerY,
28352
+ lineEndpointX: candidate.lineEndpointX,
28353
+ lineEndpointY: candidate.lineEndpointY,
28173
28354
  edgeX,
28174
28355
  edgeY,
28175
28356
  arrowOffset,
28176
- arrowStyle: snapCandidate.arrowStyle,
28177
- arrowFill: snapCandidate.arrowFill,
28357
+ arrowStyle: candidate.arrowStyle,
28358
+ arrowFill: candidate.arrowFill,
28178
28359
  solidLineEndX,
28179
28360
  solidLineEndY,
28180
28361
  arrowPoints,