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
@@ -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;
@@ -22794,6 +22871,22 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22794
22871
  this.newSize = { x: 0, y: 0, width: 0, height: 0 };
22795
22872
  this.hasResized = false;
22796
22873
  }
22874
+ /**
22875
+ * Rebase pointer and size baselines to the latest live selection bounds.
22876
+ * This prevents drift when anchored lines to external objects expand/shrink
22877
+ * the selection box while a resize gesture is in progress.
22878
+ */
22879
+ rebaseResizeBaseline(selectionGroup, clientX, clientY) {
22880
+ this.initialMouseX = clientX;
22881
+ this.initialMouseY = clientY;
22882
+ this.initialSize.width = selectionGroup.width;
22883
+ this.initialSize.height = selectionGroup.height;
22884
+ this.initialSize.x = selectionGroup.translateX;
22885
+ this.initialSize.y = selectionGroup.translateY;
22886
+ }
22887
+ shouldRebaseResizeBaseline(selectionGroup) {
22888
+ return selectionGroup.hasExternalAnchoredLineConnections?.() === true;
22889
+ }
22797
22890
  /**
22798
22891
  * Handles the pointer down event to initiate a resize operation.
22799
22892
  * Captures the initial mouse position and selection group dimensions when
@@ -22921,6 +23014,9 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22921
23014
  this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
22922
23015
  this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
22923
23016
  selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
23017
+ if (this.shouldRebaseResizeBaseline(selectionGroup)) {
23018
+ this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
23019
+ }
22924
23020
  }
22925
23021
  }
22926
23022
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
@@ -22987,6 +23083,9 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22987
23083
  this.newSize.x = newCenterX - this.newSize.width / objectScale / 2;
22988
23084
  this.newSize.y = newCenterY - this.newSize.height / objectScale / 2;
22989
23085
  selectionGroup.resize(this.newSize.x, this.newSize.y, this.newSize.width, this.newSize.height);
23086
+ if (this.shouldRebaseResizeBaseline(selectionGroup)) {
23087
+ this.rebaseResizeBaseline(selectionGroup, clientX, clientY);
23088
+ }
22990
23089
  }
22991
23090
  }
22992
23091
  }
@@ -23074,6 +23173,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23074
23173
  rotation = 0;
23075
23174
  /** The rotation value of the selection group before the current rotation operation began. */
23076
23175
  initialSelectionGroupRotation = 0;
23176
+ /** The X coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23177
+ initialCenterX = 0;
23178
+ /** The Y coordinate of the rotation pivot (group center) captured when rotation starts, in world space. */
23179
+ initialCenterY = 0;
23077
23180
  /**
23078
23181
  * Creates an instance of KritzelRotationHandler.
23079
23182
  * @param core - The KritzelCore instance that provides access to the store, engine, and other core functionalities.
@@ -23088,6 +23191,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23088
23191
  reset() {
23089
23192
  this.initialRotation = 0;
23090
23193
  this.rotation = 0;
23194
+ this.initialCenterX = 0;
23195
+ this.initialCenterY = 0;
23091
23196
  }
23092
23197
  /**
23093
23198
  * Handles the pointer down event to initiate a rotation operation.
@@ -23107,6 +23212,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23107
23212
  const objectScale = selectionGroup.scale || 1;
23108
23213
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23109
23214
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23215
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23216
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23217
+ this.initialCenterX = centerX;
23218
+ this.initialCenterY = centerY;
23110
23219
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23111
23220
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23112
23221
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23130,6 +23239,10 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23130
23239
  const objectScale = selectionGroup.scale || 1;
23131
23240
  const centerX = selectionGroup.translateX + selectionGroup.width / 2 / objectScale;
23132
23241
  const centerY = selectionGroup.translateY + selectionGroup.height / 2 / objectScale;
23242
+ // Capture the pivot once so it stays fixed even if the bounding box grows
23243
+ // mid-rotation (e.g. a line anchored to an object outside the selection).
23244
+ this.initialCenterX = centerX;
23245
+ this.initialCenterY = centerY;
23133
23246
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23134
23247
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23135
23248
  this.initialSelectionGroupRotation = selectionGroup.rotation;
@@ -23154,9 +23267,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23154
23267
  if (this._core.store.state.isRotating && selectionGroup) {
23155
23268
  const clientX = event.clientX - this._core.store.offsetX;
23156
23269
  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;
23270
+ const groupCenterX = this.initialCenterX;
23271
+ const groupCenterY = this.initialCenterY;
23160
23272
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23161
23273
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23162
23274
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23174,9 +23286,8 @@ class KritzelRotationHandler extends KritzelBaseHandler {
23174
23286
  if (this._core.store.state.isRotating && selectionGroup) {
23175
23287
  const clientX = Math.round(firstTouch.clientX - this._core.store.offsetX);
23176
23288
  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;
23289
+ const groupCenterX = this.initialCenterX;
23290
+ const groupCenterY = this.initialCenterY;
23180
23291
  const cursorX = (clientX - this._core.store.state.translateX) / this._core.store.state.scale;
23181
23292
  const cursorY = (clientY - this._core.store.state.translateY) / this._core.store.state.scale;
23182
23293
  const currentRotation = Math.atan2(groupCenterY - cursorY, groupCenterX - cursorX);
@@ -23855,6 +23966,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23855
23966
  this.hasMoved = false;
23856
23967
  this.currentSnapTarget = null;
23857
23968
  this._core.anchorManager.clearSnapCandidate();
23969
+ this._core.anchorManager.clearSnapPreviewCandidate();
23858
23970
  }
23859
23971
  /**
23860
23972
  * Handles pointer down events to initiate line handle dragging.
@@ -23957,15 +24069,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23957
24069
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
23958
24070
  // Get other endpoint's anchor to prevent anchoring both to same object
23959
24071
  const otherAnchorId = line.endAnchor?.objectId;
24072
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
23960
24073
  // Check for snap target
23961
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24074
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
23962
24075
  this.currentSnapTarget = snapTarget;
24076
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialEndX, this.initialEndY);
24077
+ let controlWorld = undefined;
24078
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24079
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24080
+ }
23963
24081
  if (snapTarget) {
23964
24082
  // Snap to target center - convert world coords to local
23965
24083
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
23966
24084
  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
24085
  // Calculate edge intersection point using AnchorManager to support curves
23970
24086
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
23971
24087
  let edgeX;
@@ -23979,30 +24095,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23979
24095
  t = clipInfo.t;
23980
24096
  }
23981
24097
  }
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
24098
  // 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
- });
24099
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24100
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'start', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24101
+ }
24102
+ else if (previewTarget) {
24103
+ const newStartX = this.initialStartX + localDx;
24104
+ const newStartY = this.initialStartY + localDy;
24105
+ this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
24106
+ this._core.anchorManager.clearSnapCandidate();
24107
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'start', previewTarget, otherEndpointWorld, controlWorld));
24006
24108
  }
24007
24109
  else {
24008
24110
  // No snap - use regular position
@@ -24011,6 +24113,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24011
24113
  this.updateLineEndpoint(line, newStartX, newStartY, this.initialEndX, this.initialEndY);
24012
24114
  // Clear snap indicator
24013
24115
  this._core.anchorManager.clearSnapCandidate();
24116
+ this._core.anchorManager.clearSnapPreviewCandidate();
24014
24117
  }
24015
24118
  }
24016
24119
  else if (handleType === 'end') {
@@ -24019,15 +24122,19 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24019
24122
  const worldY = (clientY - this._core.store.state.translateY) / viewportScale;
24020
24123
  // Get other endpoint's anchor to prevent anchoring both to same object
24021
24124
  const otherAnchorId = line.startAnchor?.objectId;
24125
+ const previewTarget = this._core.anchorManager.findSnapPreviewTarget(worldX, worldY, line.id, otherAnchorId);
24022
24126
  // Check for snap target
24023
- const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId);
24127
+ const snapTarget = this._core.anchorManager.findSnapTarget(worldX, worldY, line.id, otherAnchorId, event.pointerType);
24024
24128
  this.currentSnapTarget = snapTarget;
24129
+ const otherEndpointWorld = this.lineLocalToWorld(line, this.initialStartX, this.initialStartY);
24130
+ let controlWorld = undefined;
24131
+ if (line.controlX !== undefined && line.controlY !== undefined) {
24132
+ controlWorld = this.lineLocalToWorld(line, line.controlX, line.controlY);
24133
+ }
24025
24134
  if (snapTarget) {
24026
24135
  // Snap to target center - convert world coords to local
24027
24136
  const localCoords = this.worldToLineLocal(line, snapTarget.centerX, snapTarget.centerY);
24028
24137
  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
24138
  // Calculate edge intersection point using AnchorManager to support curves
24032
24139
  const targetObject = this._core.store.allNonSelectionObjects.find(obj => obj.id === snapTarget.objectId);
24033
24140
  let edgeX;
@@ -24041,30 +24148,16 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24041
24148
  t = clipInfo.t;
24042
24149
  }
24043
24150
  }
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
24151
  // 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
- });
24152
+ this._core.anchorManager.setSnapPreviewCandidate(null);
24153
+ this._core.anchorManager.setSnapCandidate(this.createIndicatorCandidate(line, 'end', snapTarget, otherEndpointWorld, controlWorld, { edgeX, edgeY, t }));
24154
+ }
24155
+ else if (previewTarget) {
24156
+ const newEndX = this.initialEndX + localDx;
24157
+ const newEndY = this.initialEndY + localDy;
24158
+ this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24159
+ this._core.anchorManager.clearSnapCandidate();
24160
+ this._core.anchorManager.setSnapPreviewCandidate(this.createIndicatorCandidate(line, 'end', previewTarget, otherEndpointWorld, controlWorld));
24068
24161
  }
24069
24162
  else {
24070
24163
  // No snap - use regular position
@@ -24073,9 +24166,12 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24073
24166
  this.updateLineEndpoint(line, this.initialStartX, this.initialStartY, newEndX, newEndY);
24074
24167
  // Clear snap indicator
24075
24168
  this._core.anchorManager.clearSnapCandidate();
24169
+ this._core.anchorManager.clearSnapPreviewCandidate();
24076
24170
  }
24077
24171
  }
24078
24172
  else if (handleType === 'center') {
24173
+ this._core.anchorManager.clearSnapPreviewCandidate();
24174
+ this._core.anchorManager.clearSnapCandidate();
24079
24175
  const startControlX = this.initialControlX ?? (this.initialStartX + this.initialEndX) / 2;
24080
24176
  const startControlY = this.initialControlY ?? (this.initialStartY + this.initialEndY) / 2;
24081
24177
  const newControlX = startControlX + localDx * 2;
@@ -24271,6 +24367,28 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
24271
24367
  }
24272
24368
  return null;
24273
24369
  }
24370
+ createIndicatorCandidate(line, endpoint, target, otherEndpointWorld, controlWorld, clipInfo) {
24371
+ return {
24372
+ objectId: target.objectId,
24373
+ endpoint,
24374
+ centerX: target.centerX,
24375
+ centerY: target.centerY,
24376
+ lineEndpointX: otherEndpointWorld.x,
24377
+ lineEndpointY: otherEndpointWorld.y,
24378
+ controlX: controlWorld?.x,
24379
+ controlY: controlWorld?.y,
24380
+ t: clipInfo?.t,
24381
+ edgeX: clipInfo?.edgeX,
24382
+ edgeY: clipInfo?.edgeY,
24383
+ lineStroke: KritzelColorHelper.resolveThemeColor(line.stroke),
24384
+ lineStrokeWidth: line.strokeWidth / line.scale,
24385
+ arrowOffset: endpoint === 'start'
24386
+ ? (line.hasStartArrow ? line.getArrowSize('start') / line.scale : undefined)
24387
+ : (line.hasEndArrow ? line.getArrowSize('end') / line.scale : undefined),
24388
+ arrowStyle: endpoint === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style,
24389
+ arrowFill: endpoint === 'start' ? line.getArrowFill('start') : line.getArrowFill('end'),
24390
+ };
24391
+ }
24274
24392
  }
24275
24393
 
24276
24394
  /**
@@ -27912,6 +28030,14 @@ class KritzelAnchorManager {
27912
28030
  }
27913
28031
  this.snapEndpointToObject(line, entry.endpoint, objectId);
27914
28032
  }
28033
+ const selectionGroup = this._core.store.selectionGroup;
28034
+ if (!selectionGroup || !this._core.store.state.isDragging) {
28035
+ return;
28036
+ }
28037
+ if (selectionGroup.hasExternalAnchoredLineConnections()) {
28038
+ // Keep the local drag overlay aligned with reshaped anchored lines without spamming Yjs.
28039
+ selectionGroup.refreshObjectDimensions(undefined, true);
28040
+ }
27915
28041
  }
27916
28042
  /**
27917
28043
  * Snaps a line endpoint to an object's center position.
@@ -27978,8 +28104,25 @@ class KritzelAnchorManager {
27978
28104
  * @param worldY - Y coordinate in world space
27979
28105
  * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
27980
28106
  * @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
28107
+ * @param pointerType - The pointer type driving the drag interaction.
28108
+ */
28109
+ findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, pointerType = 'mouse') {
28110
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, object => {
28111
+ const snapRadius = this.getSnapRadius(object, pointerType);
28112
+ const dx = worldX - object.centerX;
28113
+ const dy = worldY - object.centerY;
28114
+ const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
28115
+ return distanceToCenter <= snapRadius;
28116
+ });
28117
+ }
28118
+ /**
28119
+ * Finds the topmost anchorable object currently containing the dragged endpoint.
28120
+ * Used to drive preview UI before an actual snap engages.
27981
28121
  */
27982
- findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28122
+ findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
28123
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
28124
+ }
28125
+ findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
27983
28126
  let bestTarget = null;
27984
28127
  let highestZIndex = -Infinity;
27985
28128
  const objects = this._core.store.allNonSelectionObjects;
@@ -27999,28 +28142,30 @@ class KritzelAnchorManager {
27999
28142
  // Check if point is inside the object's rotated polygon
28000
28143
  const polygon = object.rotatedPolygon;
28001
28144
  const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
28002
- if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
28003
- // Only snap when the cursor is near the object's center (within 1/5 of the shorter side)
28004
- const snapRadius = Math.min(object.width, object.height) / 5;
28005
- const dx = worldX - object.centerX;
28006
- const dy = worldY - object.centerY;
28007
- const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
28008
- if (distanceToCenter > snapRadius) {
28009
- continue;
28010
- }
28011
- // If inside and close enough to center, check if this object is "above" the current best target
28012
- if (object.zIndex > highestZIndex) {
28013
- highestZIndex = object.zIndex;
28014
- bestTarget = {
28015
- objectId: object.id,
28016
- centerX: object.centerX,
28017
- centerY: object.centerY,
28018
- };
28019
- }
28145
+ if (!KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
28146
+ continue;
28147
+ }
28148
+ if (!predicate(object)) {
28149
+ continue;
28150
+ }
28151
+ if (object.zIndex > highestZIndex) {
28152
+ highestZIndex = object.zIndex;
28153
+ bestTarget = {
28154
+ objectId: object.id,
28155
+ centerX: object.centerX,
28156
+ centerY: object.centerY,
28157
+ };
28020
28158
  }
28021
28159
  }
28022
28160
  return bestTarget;
28023
28161
  }
28162
+ getSnapRadius(object, pointerType) {
28163
+ const baseRadius = Math.min(object.width, object.height) / 5;
28164
+ if (pointerType === 'touch' || pointerType === 'pen') {
28165
+ return Math.max(baseRadius * 1.75, 24 / this.getSafeScale(this._core.store.state.scale));
28166
+ }
28167
+ return baseRadius;
28168
+ }
28024
28169
  /**
28025
28170
  * Sets the current snap candidate for visual feedback during line endpoint dragging.
28026
28171
  * Updates the store state and triggers a rerender to show the snap indicator.
@@ -28031,6 +28176,17 @@ class KritzelAnchorManager {
28031
28176
  this._core.store.state.snapCandidate = candidate;
28032
28177
  this._core.rerender();
28033
28178
  }
28179
+ /**
28180
+ * Sets the current snap preview candidate for early visual feedback during line dragging.
28181
+ * Preview state must remain separate from active snap state so line clipping and anchor
28182
+ * commit semantics only react to actual snaps.
28183
+ *
28184
+ * @param candidate - The preview candidate object, or null to clear.
28185
+ */
28186
+ setSnapPreviewCandidate(candidate) {
28187
+ this._core.store.state.snapPreviewCandidate = candidate;
28188
+ this._core.rerender();
28189
+ }
28034
28190
  /**
28035
28191
  * Gets the current snap candidate from the store state.
28036
28192
  *
@@ -28039,6 +28195,14 @@ class KritzelAnchorManager {
28039
28195
  getSnapCandidate() {
28040
28196
  return this._core.store.state.snapCandidate ?? null;
28041
28197
  }
28198
+ /**
28199
+ * Gets the current snap preview candidate from the store state.
28200
+ *
28201
+ * @returns The current preview candidate if one exists, null otherwise.
28202
+ */
28203
+ getSnapPreviewCandidate() {
28204
+ return this._core.store.state.snapPreviewCandidate ?? null;
28205
+ }
28042
28206
  /**
28043
28207
  * Clears the snap candidate from the store state and triggers a rerender.
28044
28208
  * Should be called when snapping is cancelled or completed.
@@ -28047,6 +28211,13 @@ class KritzelAnchorManager {
28047
28211
  this._core.store.state.snapCandidate = null;
28048
28212
  this._core.rerender();
28049
28213
  }
28214
+ /**
28215
+ * Clears the snap preview candidate from the store state and triggers a rerender.
28216
+ */
28217
+ clearSnapPreviewCandidate() {
28218
+ this._core.store.state.snapPreviewCandidate = null;
28219
+ this._core.rerender();
28220
+ }
28050
28221
  // ============================================
28051
28222
  // Render Data
28052
28223
  // ============================================
@@ -28100,33 +28271,43 @@ class KritzelAnchorManager {
28100
28271
  * or null if there's no active snap candidate.
28101
28272
  */
28102
28273
  getSnapIndicatorRenderData() {
28103
- const snapCandidate = this.getSnapCandidate();
28104
- if (!snapCandidate)
28274
+ return this.getIndicatorRenderDataForCandidate(this.getSnapCandidate());
28275
+ }
28276
+ /**
28277
+ * Gets render data for snap preview visualization.
28278
+ *
28279
+ * @returns SnapIndicatorRenderData for the active preview candidate, or null if none exists.
28280
+ */
28281
+ getSnapPreviewRenderData() {
28282
+ return this.getIndicatorRenderDataForCandidate(this.getSnapPreviewCandidate());
28283
+ }
28284
+ getIndicatorRenderDataForCandidate(candidate) {
28285
+ if (!candidate)
28105
28286
  return null;
28106
- if (!this.areFiniteNumbers(snapCandidate.centerX, snapCandidate.centerY, snapCandidate.lineEndpointX, snapCandidate.lineEndpointY)) {
28287
+ if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
28107
28288
  return null;
28108
28289
  }
28109
28290
  const scale = this.getSafeScale(this._core.store.state.scale);
28110
28291
  const indicatorRadius = 8 / scale;
28111
28292
  const indicatorStrokeWidth = `${2 / scale}`;
28112
- const lineStrokeWidthNum = this.areFiniteNumbers(snapCandidate.lineStrokeWidth) && snapCandidate.lineStrokeWidth > 0
28113
- ? snapCandidate.lineStrokeWidth
28293
+ const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
28294
+ ? candidate.lineStrokeWidth
28114
28295
  : (4 / scale);
28115
28296
  const lineStrokeWidth = `${lineStrokeWidthNum}`;
28116
28297
  const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
28117
28298
  const dashArray = `${dashLength} ${dashLength}`;
28118
- const lineStroke = snapCandidate.lineStroke || '#000000';
28119
- let edgeX = this.areFiniteNumbers(snapCandidate.edgeX) ? snapCandidate.edgeX : undefined;
28120
- let edgeY = this.areFiniteNumbers(snapCandidate.edgeY) ? snapCandidate.edgeY : undefined;
28299
+ const lineStroke = candidate.lineStroke || '#000000';
28300
+ let edgeX = this.areFiniteNumbers(candidate.edgeX) ? candidate.edgeX : undefined;
28301
+ let edgeY = this.areFiniteNumbers(candidate.edgeY) ? candidate.edgeY : undefined;
28121
28302
  let solidLineEndX = edgeX;
28122
28303
  let solidLineEndY = edgeY;
28123
28304
  let arrowPoints;
28124
- const arrowOffset = this.areFiniteNumbers(snapCandidate.arrowOffset) && snapCandidate.arrowOffset > 0
28125
- ? snapCandidate.arrowOffset
28305
+ const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
28306
+ ? candidate.arrowOffset
28126
28307
  : undefined;
28127
28308
  if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
28128
- const dx = snapCandidate.lineEndpointX - edgeX;
28129
- const dy = snapCandidate.lineEndpointY - edgeY;
28309
+ const dx = candidate.lineEndpointX - edgeX;
28310
+ const dy = candidate.lineEndpointY - edgeY;
28130
28311
  const length = Math.sqrt(dx * dx + dy * dy);
28131
28312
  if (length > arrowOffset) {
28132
28313
  solidLineEndX = edgeX + (dx / length) * arrowOffset;
@@ -28134,8 +28315,8 @@ class KritzelAnchorManager {
28134
28315
  }
28135
28316
  // Calculate arrow head points
28136
28317
  // Direction from line endpoint to edge (arrow direction)
28137
- const arrowDx = edgeX - snapCandidate.lineEndpointX;
28138
- const arrowDy = edgeY - snapCandidate.lineEndpointY;
28318
+ const arrowDx = edgeX - candidate.lineEndpointX;
28319
+ const arrowDy = edgeY - candidate.lineEndpointY;
28139
28320
  const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
28140
28321
  if (arrowLengthTotal > 0) {
28141
28322
  const ux = arrowDx / arrowLengthTotal;
@@ -28166,15 +28347,15 @@ class KritzelAnchorManager {
28166
28347
  return null;
28167
28348
  }
28168
28349
  const snapLinePath = (() => {
28169
- if (snapCandidate.controlX !== undefined &&
28170
- snapCandidate.controlY !== undefined &&
28171
- snapCandidate.t !== undefined &&
28172
- this.areFiniteNumbers(snapCandidate.controlX, snapCandidate.controlY, snapCandidate.t)) {
28173
- const startT = snapCandidate.endpoint === 'start' ? 1 - snapCandidate.t : snapCandidate.t;
28350
+ if (candidate.controlX !== undefined &&
28351
+ candidate.controlY !== undefined &&
28352
+ candidate.t !== undefined &&
28353
+ this.areFiniteNumbers(candidate.controlX, candidate.controlY, candidate.t)) {
28354
+ const startT = candidate.endpoint === 'start' ? 1 - candidate.t : candidate.t;
28174
28355
  // Ensure meaningful range
28175
28356
  if (startT >= 1)
28176
28357
  return undefined;
28177
- const segment = this.extractQuadraticSegment({ x: snapCandidate.lineEndpointX, y: snapCandidate.lineEndpointY }, { x: snapCandidate.controlX, y: snapCandidate.controlY }, { x: snapCandidate.centerX, y: snapCandidate.centerY }, startT, 1);
28358
+ const segment = this.extractQuadraticSegment({ x: candidate.lineEndpointX, y: candidate.lineEndpointY }, { x: candidate.controlX, y: candidate.controlY }, { x: candidate.centerX, y: candidate.centerY }, startT, 1);
28178
28359
  if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
28179
28360
  return undefined;
28180
28361
  }
@@ -28188,15 +28369,15 @@ class KritzelAnchorManager {
28188
28369
  lineStrokeWidth,
28189
28370
  dashArray,
28190
28371
  lineStroke,
28191
- centerX: snapCandidate.centerX,
28192
- centerY: snapCandidate.centerY,
28193
- lineEndpointX: snapCandidate.lineEndpointX,
28194
- lineEndpointY: snapCandidate.lineEndpointY,
28372
+ centerX: candidate.centerX,
28373
+ centerY: candidate.centerY,
28374
+ lineEndpointX: candidate.lineEndpointX,
28375
+ lineEndpointY: candidate.lineEndpointY,
28195
28376
  edgeX,
28196
28377
  edgeY,
28197
28378
  arrowOffset,
28198
- arrowStyle: snapCandidate.arrowStyle,
28199
- arrowFill: snapCandidate.arrowFill,
28379
+ arrowStyle: candidate.arrowStyle,
28380
+ arrowFill: candidate.arrowFill,
28200
28381
  solidLineEndX,
28201
28382
  solidLineEndY,
28202
28383
  arrowPoints,