kritzel-stencil 0.3.25 → 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 (39) 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-CPz_o--2.js} +267 -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/rotation.handler.js +18 -6
  7. package/dist/collection/classes/managers/anchor.manager.js +108 -45
  8. package/dist/collection/classes/objects/selection-group.class.js +80 -5
  9. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +10 -6
  10. package/dist/collection/configs/default-engine-config.js +1 -0
  11. package/dist/collection/constants/version.js +1 -1
  12. package/dist/components/index.js +1 -1
  13. package/dist/components/kritzel-controls.js +1 -1
  14. package/dist/components/kritzel-editor.js +1 -1
  15. package/dist/components/kritzel-engine.js +1 -1
  16. package/dist/components/kritzel-settings.js +1 -1
  17. package/dist/components/kritzel-tool-config.js +1 -1
  18. package/dist/components/p-B0VgBZUT.js +9 -0
  19. package/dist/components/{p-DF0S4FqL.js → p-BAhZcKRe.js} +1 -1
  20. package/dist/components/{p-Dw-t1qQc.js → p-DKxt_AjI.js} +1 -1
  21. package/dist/components/{p-B5XGjTLd.js → p-DRncK1E6.js} +1 -1
  22. package/dist/components/{p-DNDt6O6A.js → p-DUo2M5uM.js} +1 -1
  23. package/dist/esm/index.js +2 -2
  24. package/dist/esm/kritzel-active-users_45.entry.js +13 -8
  25. package/dist/esm/{schema.constants-1SDhlkfS.js → schema.constants-C9ZX7hQv.js} +267 -108
  26. package/dist/stencil/index.esm.js +1 -1
  27. package/dist/stencil/p-71271db1.entry.js +9 -0
  28. package/dist/stencil/{p-1SDhlkfS.js → p-C9ZX7hQv.js} +1 -1
  29. package/dist/stencil/stencil.esm.js +1 -1
  30. package/dist/types/classes/handlers/line-handle.handler.d.ts +1 -0
  31. package/dist/types/classes/handlers/rotation.handler.d.ts +4 -0
  32. package/dist/types/classes/managers/anchor.manager.d.ts +35 -2
  33. package/dist/types/classes/objects/selection-group.class.d.ts +8 -1
  34. package/dist/types/constants/version.d.ts +1 -1
  35. package/dist/types/interfaces/anchor.interface.d.ts +12 -2
  36. package/dist/types/interfaces/engine-state.interface.d.ts +2 -1
  37. package/package.json +1 -1
  38. package/dist/components/p-6_95PFq4.js +0 -9
  39. 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;
@@ -23052,6 +23129,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23052
23129
  rotation = 0;
23053
23130
  /** The rotation value of the selection group before the current rotation operation began. */
23054
23131
  initialSelectionGroupRotation = 0;
23132
+ /** The X coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23133
+ initialCenterX = 0;
23134
+ /** The Y coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23135
+ initialCenterY = 0;
23055
23136
  /**
23056
23137
  * Creates an instance of KritzelRotationHandler.
23057
23138
  * @param core - The KritzelCore instance that provides access to the store, engine, and other core functionalities.
@@ -23066,6 +23147,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23066
23147
  reset() {
23067
23148
  this.initialRotation = 0;
23068
23149
  this.rotation = 0;
23150
+ this.initialCenterX = 0;
23151
+ this.initialCenterY = 0;
23069
23152
  }
23070
23153
  /**
23071
23154
  * Handles the pointer down event to initiate a rotation operation.
@@ -23085,6 +23168,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23085
23168
  const objectScale = selectionGroup.scale || 1;
23086
23169
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23087
23170
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23171
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23172
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23173
+ this.initialCenterX = centerX;
23174
+ this.initialCenterY = centerY;
23088
23175
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23089
23176
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23090
23177
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23108,6 +23195,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23108
23195
  const objectScale = selectionGroup.scale || 1;
23109
23196
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23110
23197
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23198
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23199
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23200
+ this.initialCenterX = centerX;
23201
+ this.initialCenterY = centerY;
23111
23202
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23112
23203
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23113
23204
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23132,9 +23223,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23132
23223
  if (this._core.store.state.isRotating && selectionGroup) {
23133
23224
  const clientX = event.clientX - this._core.store.offsetX;
23134
23225
  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;
23226
+ const groupCenterX = this.initialCenterX;
23227
+ const groupCenterY = this.initialCenterY;
23138
23228
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23139
23229
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23140
23230
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23152,9 +23242,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23152
23242
  if (this._core.store.state.isRotating && selectionGroup) {
23153
23243
  const clientX = Math.round(firstTouch.clientX - this._core.store.offsetX);
23154
23244
  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;
23245
+ const groupCenterX = this.initialCenterX;
23246
+ const groupCenterY = this.initialCenterY;
23158
23247
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23159
23248
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23160
23249
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23833,6 +23922,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23833
23922
  this.hasMoved = false;
23834
23923
  this.currentSnapTarget = null;
23835
23924
  this._core.anchorManager.clearSnapCandidate();
23925
+ this._core.anchorManager.clearSnapPreviewCandidate();
23836
23926
  }
23837
23927
  /**
23838
23928
  * Handles pointer down events to initiate line handle dragging.
@@ -23935,15 +24025,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23935
24025
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23936
24026
  // Get other endpoint's anchor to prevent anchoring both to same object
23937
24027
  const otherAnchorId = line.endAnchor?.objectId;
24028
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
23938
24029
  // Check for snap target
23939
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24030
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
23940
24031
  this.currentSnapTarget = snapTarget;
24032
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
24033
+ let controlWorld = undefined;
24034
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24035
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24036
+ }
23941
24037
  if (snapTarget) {
23942
24038
  // Snap to target center - convert world coords to local
23943
24039
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
23944
24040
  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
24041
  // Calculate edge intersection point using AnchorManager to support curves
23948
24042
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
23949
24043
  let edgeX;
@@ -23957,30 +24051,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23957
24051
  t = clipInfo.t;
23958
24052
  }
23959
24053
  }
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
24054
  // 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
- });
24055
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24056
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'start', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24057
+ }
24058
+ else if (previewTarget) {
24059
+ const newStartX = this.initialStartX + localDx;
24060
+ const newStartY = this.initialStartY + localDy;
24061
+ this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
24062
+ this._core.anchorManager.clearSnapCandidate();
24063
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'start', previewTarget, otherEndpointWorld, controlWorld));
23984
24064
  }
23985
24065
  else {
23986
24066
  // No snap - use regular position
@@ -23989,6 +24069,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23989
24069
  this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
23990
24070
  // Clear snap indicator
23991
24071
  this._core.anchorManager.clearSnapCandidate();
24072
+ this._core.anchorManager.clearSnapPreviewCandidate();
23992
24073
  }
23993
24074
  }
23994
24075
  else if (handleType === 'end') {
@@ -23997,15 +24078,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23997
24078
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23998
24079
  // Get other endpoint's anchor to prevent anchoring both to same object
23999
24080
  const otherAnchorId = line.startAnchor?.objectId;
24081
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
24000
24082
  // Check for snap target
24001
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24083
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
24002
24084
  this.currentSnapTarget = snapTarget;
24085
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
24086
+ let controlWorld = undefined;
24087
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24088
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24089
+ }
24003
24090
  if (snapTarget) {
24004
24091
  // Snap to target center - convert world coords to local
24005
24092
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
24006
24093
  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
24094
  // Calculate edge intersection point using AnchorManager to support curves
24010
24095
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
24011
24096
  let edgeX;
@@ -24019,30 +24104,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24019
24104
  t = clipInfo.t;
24020
24105
  }
24021
24106
  }
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
24107
  // 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
- });
24108
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24109
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'end', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24110
+ }
24111
+ else if (previewTarget) {
24112
+ const newEndX = this.initialEndX + localDx;
24113
+ const newEndY = this.initialEndY + localDy;
24114
+ this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24115
+ this._core.anchorManager.clearSnapCandidate();
24116
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'end', previewTarget, otherEndpointWorld, controlWorld));
24046
24117
  }
24047
24118
  else {
24048
24119
  // No snap - use regular position
@@ -24051,9 +24122,12 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24051
24122
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24052
24123
  // Clear snap indicator
24053
24124
  this._core.anchorManager.clearSnapCandidate();
24125
+ this._core.anchorManager.clearSnapPreviewCandidate();
24054
24126
  }
24055
24127
  }
24056
24128
  else if (handleType === 'center') {
24129
+ this._core.anchorManager.clearSnapPreviewCandidate();
24130
+ this._core.anchorManager.clearSnapCandidate();
24057
24131
  const startControlX = this.initialControlX ?? (this.initialStartX + this.initialEndX) / 2;
24058
24132
  const startControlY = this.initialControlY ?? (this.initialStartY + this.initialEndY) / 2;
24059
24133
  const newControlX = startControlX + localDx * 2;
@@ -24249,6 +24323,28 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24249
24323
  }
24250
24324
  return null;
24251
24325
  }
24326
+ createIndicatorCandidate(line, endpoint, target, otherEndpointWorld, controlWorld, clipInfo) {
24327
+ return {
24328
+ objectId: target.objectId,
24329
+ endpoint,
24330
+ centerX: target.centerX,
24331
+ centerY: target.centerY,
24332
+ lineEndpointX: otherEndpointWorld.x,
24333
+ lineEndpointY: otherEndpointWorld.y,
24334
+ controlX: controlWorld?.x,
24335
+ controlY: controlWorld?.y,
24336
+ t: clipInfo?.t,
24337
+ edgeX: clipInfo?.edgeX,
24338
+ edgeY: clipInfo?.edgeY,
24339
+ lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24340
+ lineStrokeWidth: line.strokeWidth / line.scale,
24341
+ arrowOffset: endpoint === 'start'
24342
+ ? (line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined)
24343
+ : (line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined),
24344
+ arrowStyle: endpoint === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style,
24345
+ arrowFill: endpoint === 'start' ? line.getArrowFill('start') : line.getArrowFill('end'),
24346
+ };
24347
+ }
24252
24348
  }
24253
24349
 
24254
24350
  /**
@@ -27890,6 +27986,14 @@ class KritzelAnchorManager {
27890
27986
  }
27891
27987
  this.snapEndpointToObject(line, entry.endpoint, objectId);
27892
27988
  }
27989
+ const selectionGroup = this._core.store.selectionGroup;
27990
+ if (!selectionGroup || !this._core.store.state.isDragging) {
27991
+ return;
27992
+ }
27993
+ if (selectionGroup.hasExternalAnchoredLineConnections()) {
27994
+ // Keep the local drag overlay aligned with reshaped anchored lines without spamming Yjs.
27995
+ selectionGroup.refreshObjectDimensions(undefined, true);
27996
+ }
27893
27997
  }
27894
27998
  /**
27895
27999
  * Snaps a line endpoint to an object's center position.
@@ -27956,8 +28060,25 @@ class KritzelAnchorManager {
27956
28060
  * @param worldY - Y coordinate in world space
27957
28061
  * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
27958
28062
  * @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
28063
+ * @param pointerType - The pointer type driving the drag interaction.
28064
+ */
28065
+ findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, pointerType = 'mouse') {
28066
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, object => {
28067
+ const snapRadius = this.getSnapRadius(object, pointerType);
28068
+ const dx = worldX - object.centerX;
28069
+ const dy = worldY - object.centerY;
28070
+ const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
28071
+ return distanceToCenter <= snapRadius;
28072
+ });
28073
+ }
28074
+ /**
28075
+ * Finds the topmost anchorable object currently containing the dragged endpoint.
28076
+ * Used to drive preview UI before an actual snap engages.
27959
28077
  */
27960
- findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28078
+ findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28079
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
28080
+ }
28081
+ findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
27961
28082
  let bestTarget = null;
27962
28083
  let highestZIndex = -Infinity;
27963
28084
  const objects = this._core.store.allNonSelectionObjects;
@@ -27977,28 +28098,30 @@ class KritzelAnchorManager {
27977
28098
  // Check if point is inside the object's rotated polygon
27978
28099
  const polygon = object.rotatedPolygon;
27979
28100
  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
- }
28101
+ if (!KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
28102
+ continue;
28103
+ }
28104
+ if (!predicate(object)) {
28105
+ continue;
28106
+ }
28107
+ if (object.zIndex > highestZIndex) {
28108
+ highestZIndex = object.zIndex;
28109
+ bestTarget = {
28110
+ objectId: object.id,
28111
+ centerX: object.centerX,
28112
+ centerY: object.centerY,
28113
+ };
27998
28114
  }
27999
28115
  }
28000
28116
  return bestTarget;
28001
28117
  }
28118
+ getSnapRadius(object, pointerType) {
28119
+ const baseRadius = Math.min(object.width, object.height) / 5;
28120
+ if (pointerType === 'touch' || pointerType === 'pen') {
28121
+ return Math.max(baseRadius * 1.75, 24 / this.getSafeScale(this._core.store.state.scale));
28122
+ }
28123
+ return baseRadius;
28124
+ }
28002
28125
  /**
28003
28126
  * Sets the current snap candidate for visual feedback during line endpoint dragging.
28004
28127
  * Updates the store state and triggers a rerender to show the snap indicator.
@@ -28009,6 +28132,17 @@ class KritzelAnchorManager {
28009
28132
  this._core.store.state.snapCandidate = candidate;
28010
28133
  this._core.rerender();
28011
28134
  }
28135
+ /**
28136
+ * Sets the current snap preview candidate for early visual feedback during line dragging.
28137
+ * Preview state must remain separate from active snap state so line clipping and anchor
28138
+ * commit semantics only react to actual snaps.
28139
+ *
28140
+ * @param candidate - The preview candidate object, or null to clear.
28141
+ */
28142
+ setSnapPreviewCandidate(candidate) {
28143
+ this._core.store.state.snapPreviewCandidate = candidate;
28144
+ this._core.rerender();
28145
+ }
28012
28146
  /**
28013
28147
  * Gets the current snap candidate from the store state.
28014
28148
  *
@@ -28017,6 +28151,14 @@ class KritzelAnchorManager {
28017
28151
  getSnapCandidate() {
28018
28152
  return this._core.store.state.snapCandidate ?? null;
28019
28153
  }
28154
+ /**
28155
+ * Gets the current snap preview candidate from the store state.
28156
+ *
28157
+ * @returns The current preview candidate if one exists, null otherwise.
28158
+ */
28159
+ getSnapPreviewCandidate() {
28160
+ return this._core.store.state.snapPreviewCandidate ?? null;
28161
+ }
28020
28162
  /**
28021
28163
  * Clears the snap candidate from the store state and triggers a rerender.
28022
28164
  * Should be called when snapping is cancelled or completed.
@@ -28025,6 +28167,13 @@ class KritzelAnchorManager {
28025
28167
  this._core.store.state.snapCandidate = null;
28026
28168
  this._core.rerender();
28027
28169
  }
28170
+ /**
28171
+ * Clears the snap preview candidate from the store state and triggers a rerender.
28172
+ */
28173
+ clearSnapPreviewCandidate() {
28174
+ this._core.store.state.snapPreviewCandidate = null;
28175
+ this._core.rerender();
28176
+ }
28028
28177
  // ============================================
28029
28178
  // Render Data
28030
28179
  // ============================================
@@ -28078,33 +28227,43 @@ class KritzelAnchorManager {
28078
28227
  * or null if there's no active snap candidate.
28079
28228
  */
28080
28229
  getSnapIndicatorRenderData() {
28081
- const snapCandidate = this.getSnapCandidate();
28082
- if (!snapCandidate)
28230
+ return this.getIndicatorRenderDataForCandidate(this.getSnapCandidate());
28231
+ }
28232
+ /**
28233
+ * Gets render data for snap preview visualization.
28234
+ *
28235
+ * @returns SnapIndicatorRenderData for the active preview candidate, or null if none exists.
28236
+ */
28237
+ getSnapPreviewRenderData() {
28238
+ return this.getIndicatorRenderDataForCandidate(this.getSnapPreviewCandidate());
28239
+ }
28240
+ getIndicatorRenderDataForCandidate(candidate) {
28241
+ if (!candidate)
28083
28242
  return null;
28084
- if (!this.areFiniteNumbers(snapCandidate.centerX, snapCandidate.centerY, snapCandidate.lineEndpointX, snapCandidate.lineEndpointY)) {
28243
+ if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
28085
28244
  return null;
28086
28245
  }
28087
28246
  const scale = this.getSafeScale(this._core.store.state.scale);
28088
28247
  const indicatorRadius = 8 / scale;
28089
28248
  const indicatorStrokeWidth = `${2 / scale}`;
28090
- const lineStrokeWidthNum = this.areFiniteNumbers(snapCandidate.lineStrokeWidth) && snapCandidate.lineStrokeWidth > 0
28091
- ? snapCandidate.lineStrokeWidth
28249
+ const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
28250
+ ? candidate.lineStrokeWidth
28092
28251
  : (4 / scale);
28093
28252
  const lineStrokeWidth = `${lineStrokeWidthNum}`;
28094
28253
  const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
28095
28254
  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;
28255
+ const lineStroke = candidate.lineStroke || '#000000';
28256
+ let edgeX = this.areFiniteNumbers(candidate.edgeX) ? candidate.edgeX : undefined;
28257
+ let edgeY = this.areFiniteNumbers(candidate.edgeY) ? candidate.edgeY : undefined;
28099
28258
  let solidLineEndX = edgeX;
28100
28259
  let solidLineEndY = edgeY;
28101
28260
  let arrowPoints;
28102
- const arrowOffset = this.areFiniteNumbers(snapCandidate.arrowOffset) && snapCandidate.arrowOffset > 0
28103
- ? snapCandidate.arrowOffset
28261
+ const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
28262
+ ? candidate.arrowOffset
28104
28263
  : undefined;
28105
28264
  if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
28106
- const dx = snapCandidate.lineEndpointX - edgeX;
28107
- const dy = snapCandidate.lineEndpointY - edgeY;
28265
+ const dx = candidate.lineEndpointX - edgeX;
28266
+ const dy = candidate.lineEndpointY - edgeY;
28108
28267
  const length = Math.sqrt(dx * dx + dy * dy);
28109
28268
  if (length > arrowOffset) {
28110
28269
  solidLineEndX = edgeX + (dx / length) * arrowOffset;
@@ -28112,8 +28271,8 @@ class KritzelAnchorManager {
28112
28271
  }
28113
28272
  // Calculate arrow head points
28114
28273
  // Direction from line endpoint to edge (arrow direction)
28115
- const arrowDx = edgeX - snapCandidate.lineEndpointX;
28116
- const arrowDy = edgeY - snapCandidate.lineEndpointY;
28274
+ const arrowDx = edgeX - candidate.lineEndpointX;
28275
+ const arrowDy = edgeY - candidate.lineEndpointY;
28117
28276
  const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
28118
28277
  if (arrowLengthTotal > 0) {
28119
28278
  const ux = arrowDx / arrowLengthTotal;
@@ -28144,15 +28303,15 @@ class KritzelAnchorManager {
28144
28303
  return null;
28145
28304
  }
28146
28305
  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;
28306
+ if (candidate.controlX !== undefined &&
28307
+ candidate.controlY !== undefined &&
28308
+ candidate.t !== undefined &&
28309
+ this.areFiniteNumbers(candidate.controlX, candidate.controlY, candidate.t)) {
28310
+ const startT = candidate.endpoint === 'start' ? 1 - candidate.t : candidate.t;
28152
28311
  // Ensure meaningful range
28153
28312
  if (startT >= 1)
28154
28313
  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);
28314
+ 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
28315
  if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
28157
28316
  return undefined;
28158
28317
  }
@@ -28166,15 +28325,15 @@ class KritzelAnchorManager {
28166
28325
  lineStrokeWidth,
28167
28326
  dashArray,
28168
28327
  lineStroke,
28169
- centerX: snapCandidate.centerX,
28170
- centerY: snapCandidate.centerY,
28171
- lineEndpointX: snapCandidate.lineEndpointX,
28172
- lineEndpointY: snapCandidate.lineEndpointY,
28328
+ centerX: candidate.centerX,
28329
+ centerY: candidate.centerY,
28330
+ lineEndpointX: candidate.lineEndpointX,
28331
+ lineEndpointY: candidate.lineEndpointY,
28173
28332
  edgeX,
28174
28333
  edgeY,
28175
28334
  arrowOffset,
28176
- arrowStyle: snapCandidate.arrowStyle,
28177
- arrowFill: snapCandidate.arrowFill,
28335
+ arrowStyle: candidate.arrowStyle,
28336
+ arrowFill: candidate.arrowFill,
28178
28337
  solidLineEndX,
28179
28338
  solidLineEndY,
28180
28339
  arrowPoints,