kritzel-stencil 0.3.23 → 0.3.25

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 (32) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_45.cjs.entry.js +55 -36
  3. package/dist/cjs/{schema.constants-Lr_zNkox.js → schema.constants-Np7j2Gz4.js} +44 -4
  4. package/dist/collection/classes/handlers/selection.handler.js +15 -2
  5. package/dist/collection/classes/managers/anchor.manager.js +13 -2
  6. package/dist/collection/classes/managers/clipboard.manager.js +7 -0
  7. package/dist/collection/classes/objects/selection-group.class.js +17 -0
  8. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +46 -34
  9. package/dist/collection/constants/version.js +1 -1
  10. package/dist/components/index.js +1 -1
  11. package/dist/components/kritzel-controls.js +1 -1
  12. package/dist/components/kritzel-editor.js +1 -1
  13. package/dist/components/kritzel-engine.js +1 -1
  14. package/dist/components/kritzel-settings.js +1 -1
  15. package/dist/components/kritzel-tool-config.js +1 -1
  16. package/dist/components/p-6_95PFq4.js +9 -0
  17. package/dist/components/{p-CPMpf9ku.js → p-B5XGjTLd.js} +1 -1
  18. package/dist/components/{p-BYveczcb.js → p-DF0S4FqL.js} +1 -1
  19. package/dist/components/{p-DJ3KI725.js → p-DNDt6O6A.js} +1 -1
  20. package/dist/components/{p-DTqMF94b.js → p-Dw-t1qQc.js} +1 -1
  21. package/dist/esm/index.js +2 -2
  22. package/dist/esm/kritzel-active-users_45.entry.js +55 -36
  23. package/dist/esm/{schema.constants-DMPBDrZh.js → schema.constants-1SDhlkfS.js} +44 -4
  24. package/dist/stencil/index.esm.js +1 -1
  25. package/dist/stencil/{p-DMPBDrZh.js → p-1SDhlkfS.js} +1 -1
  26. package/dist/stencil/{p-27d41f6f.entry.js → p-7ba7df62.entry.js} +2 -2
  27. package/dist/stencil/stencil.esm.js +1 -1
  28. package/dist/types/classes/managers/anchor.manager.d.ts +4 -1
  29. package/dist/types/classes/objects/selection-group.class.d.ts +6 -0
  30. package/dist/types/constants/version.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/dist/components/p-C48bv_0f.js +0 -9
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var schema_constants = require('./schema.constants-Lr_zNkox.js');
3
+ var schema_constants = require('./schema.constants-Np7j2Gz4.js');
4
4
  var Y = require('yjs');
5
5
  var yIndexeddb = require('y-indexeddb');
6
6
  var yWebsocket = require('y-websocket');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var index = require('./index-Xav9JFHg.js');
4
- var schema_constants = require('./schema.constants-Lr_zNkox.js');
4
+ var schema_constants = require('./schema.constants-Np7j2Gz4.js');
5
5
  var Y = require('yjs');
6
6
  require('y-indexeddb');
7
7
  require('y-websocket');
@@ -24157,6 +24157,7 @@ class KritzelClipboardManager {
24157
24157
  */
24158
24158
  regenerateIdsAndRemapAnchors(topLevel) {
24159
24159
  const all = [];
24160
+ const hasOnlyTopLevelLines = topLevel.length > 0 && topLevel.every(object => schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelLine'));
24160
24161
  const collect = (objects) => {
24161
24162
  for (const object of objects) {
24162
24163
  all.push(object);
@@ -24181,9 +24182,15 @@ class KritzelClipboardManager {
24181
24182
  if (object.startAnchor && idMap.has(object.startAnchor.objectId)) {
24182
24183
  object.startAnchor = { objectId: idMap.get(object.startAnchor.objectId) };
24183
24184
  }
24185
+ else if (hasOnlyTopLevelLines) {
24186
+ object.startAnchor = undefined;
24187
+ }
24184
24188
  if (object.endAnchor && idMap.has(object.endAnchor.objectId)) {
24185
24189
  object.endAnchor = { objectId: idMap.get(object.endAnchor.objectId) };
24186
24190
  }
24191
+ else if (hasOnlyTopLevelLines) {
24192
+ object.endAnchor = undefined;
24193
+ }
24187
24194
  }
24188
24195
  }
24189
24196
  }
@@ -29412,7 +29419,11 @@ const KritzelEngine = class {
29412
29419
  transform: object?.transformationMatrix,
29413
29420
  transformOrigin: 'top left',
29414
29421
  position: 'absolute',
29415
- zIndex: object.zIndex.toString(),
29422
+ // The object wrapper establishes its own stacking context (transform + z-index),
29423
+ // so the selection-line handles inside it cannot escape above the snap/anchor
29424
+ // layers (z-index 9999/9998) rendered later at the origin level. Lift the
29425
+ // selected line's wrapper above those layers so its handles always paint on top.
29426
+ zIndex: this.core.displaySelectionLineUI(object) ? '10001' : object.zIndex.toString(),
29416
29427
  pointerEvents: getObjectWrapperPointerEvents(object),
29417
29428
  } }, schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelPath') && (index.h("svg", { ref: el => el && object.mount(el), xmlns: "http://www.w3.org/2000/svg", style: {
29418
29429
  height: object?.totalHeight + 'px',
@@ -29616,10 +29627,19 @@ const KritzelEngine = class {
29616
29627
  }
29617
29628
  const selectionBorderColor = remoteUserColor ?? (schema_constants.KritzelColorHelper.resolveThemeColor(object.borderColor, currentTheme) || 'var(--kritzel-selection-border-color, #007AFF)');
29618
29629
  const selectionHandleStrokeColor = remoteUserColor ?? 'var(--kritzel-selection-handle-stroke-color, #007AFF)';
29630
+ const lineObject = object;
29631
+ const activeSnapCandidate = this.core.store.state.snapCandidate;
29632
+ const isStartHandleSnapped = lineObject.startAnchor != null;
29633
+ const isEndHandleSnapped = lineObject.endAnchor != null;
29634
+ const isStartHandleSnapPreview = activeSnapCandidate?.endpoint === 'start';
29635
+ const isEndHandleSnapPreview = activeSnapCandidate?.endpoint === 'end';
29636
+ const isStartHandleHighlighted = isStartHandleSnapped || isStartHandleSnapPreview;
29637
+ const isEndHandleHighlighted = isEndHandleSnapped || isEndHandleSnapPreview;
29638
+ const selectionOverlayZIndex = this.core.displaySelectionLineUI(object) ? '10000' : (object.zIndex + 1).toString();
29619
29639
  const handleScale = object.scale ?? 1;
29620
29640
  const { canRenderResizeHandles, canRenderRotationHandle } = this.getSelectionHandleCapabilities(object);
29621
29641
  return (index.h("svg", { xmlns: "http://www.w3.org/2000/svg", style: {
29622
- zIndex: (object.zIndex + 1).toString(),
29642
+ zIndex: selectionOverlayZIndex,
29623
29643
  height: object?.totalHeight.toString(),
29624
29644
  width: object?.totalWidth.toString(),
29625
29645
  left: '0',
@@ -29710,7 +29730,7 @@ const KritzelEngine = class {
29710
29730
  strokeLinecap: 'round',
29711
29731
  fill: 'none',
29712
29732
  } })), !this.isSelecting && !isRemoteSelection && (index.h("g", { class: "selection-line-handles", style: { pointerEvents: 'auto' } }, index.h("circle", { class: "selection-line-handle start", cx: object.startX - object.x, cy: object.startY - object.y, r: `${((baseHandleSize - 1) * object.scale) / this.core.store.state?.scale}`, style: {
29713
- fill: 'var(--kritzel-selection-handle-color, #000000)',
29733
+ fill: isStartHandleHighlighted ? selectionHandleStrokeColor : 'var(--kritzel-selection-handle-color, #000000)',
29714
29734
  stroke: selectionHandleStrokeColor,
29715
29735
  strokeWidth: `calc(2px * ${object.scale} / ${this.core.store.state?.scale})`,
29716
29736
  paintOrder: 'fill',
@@ -29734,7 +29754,7 @@ const KritzelEngine = class {
29734
29754
  fill: 'transparent',
29735
29755
  paintOrder: 'fill',
29736
29756
  } }), index.h("circle", { class: "selection-line-handle end", cx: object.endX - object.x, cy: object.endY - object.y, r: `${((baseHandleSize - 1) * object.scale) / this.core.store.state?.scale}`, style: {
29737
- fill: 'var(--kritzel-selection-handle-color, #000000)',
29757
+ fill: isEndHandleHighlighted ? selectionHandleStrokeColor : 'var(--kritzel-selection-handle-color, #000000)',
29738
29758
  stroke: selectionHandleStrokeColor,
29739
29759
  strokeWidth: `calc(2px * ${object.scale} / ${this.core.store.state?.scale})`,
29740
29760
  paintOrder: 'fill',
@@ -29744,6 +29764,34 @@ const KritzelEngine = class {
29744
29764
  } })))))));
29745
29765
  })()));
29746
29766
  }), (() => {
29767
+ const data = this.core.anchorManager.getSnapIndicatorRenderData();
29768
+ if (!data)
29769
+ return null;
29770
+ return (index.h("svg", { xmlns: "http://www.w3.org/2000/svg", class: "snap-indicator", style: {
29771
+ position: 'absolute',
29772
+ left: '0',
29773
+ top: '0',
29774
+ width: '1px',
29775
+ height: '1px',
29776
+ pointerEvents: 'none',
29777
+ zIndex: '9999',
29778
+ overflow: 'visible',
29779
+ } }, index.h("g", null, data.snapLinePath ? (index.h("path", { d: data.snapLinePath, fill: "none", style: {
29780
+ stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
29781
+ strokeWidth: data.lineStrokeWidth,
29782
+ strokeDasharray: data.dashArray,
29783
+ strokeLinecap: 'round',
29784
+ } })) : (data.edgeX !== undefined &&
29785
+ data.edgeY !== undefined && (index.h("line", { x1: data.edgeX, y1: data.edgeY, x2: data.centerX, y2: data.centerY, style: {
29786
+ stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
29787
+ strokeWidth: data.lineStrokeWidth,
29788
+ strokeDasharray: data.dashArray,
29789
+ strokeLinecap: 'round',
29790
+ } }))), index.h("circle", { cx: data.centerX, cy: data.centerY, r: data.indicatorRadius, style: {
29791
+ fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
29792
+ strokeWidth: data.indicatorStrokeWidth,
29793
+ } }))));
29794
+ })(), (() => {
29747
29795
  const data = this.core.anchorManager.getAnchorLinesRenderData();
29748
29796
  if (!data)
29749
29797
  return null;
@@ -29768,7 +29816,7 @@ const KritzelEngine = class {
29768
29816
  strokeDasharray: data.dashArray,
29769
29817
  strokeLinecap: 'round',
29770
29818
  } })), index.h("circle", { cx: data.startAnchorViz.centerX, cy: data.startAnchorViz.centerY, r: data.indicatorRadius, style: {
29771
- fill: 'var(--kritzel-snap-indicator-fill, rgba(0, 0, 0))',
29819
+ fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
29772
29820
  stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
29773
29821
  strokeWidth: data.indicatorStrokeWidth,
29774
29822
  } }))), data.endAnchorViz && (index.h("g", { class: "anchor-line-end" }, data.endAnchorViz.pathD ? (index.h("path", { d: data.endAnchorViz.pathD, style: {
@@ -29787,35 +29835,6 @@ const KritzelEngine = class {
29787
29835
  stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
29788
29836
  strokeWidth: data.indicatorStrokeWidth,
29789
29837
  } })))));
29790
- })(), (() => {
29791
- const data = this.core.anchorManager.getSnapIndicatorRenderData();
29792
- if (!data)
29793
- return null;
29794
- return (index.h("svg", { xmlns: "http://www.w3.org/2000/svg", class: "snap-indicator", style: {
29795
- position: 'absolute',
29796
- left: '0',
29797
- top: '0',
29798
- width: '1px',
29799
- height: '1px',
29800
- pointerEvents: 'none',
29801
- zIndex: '9999',
29802
- overflow: 'visible',
29803
- } }, index.h("g", null, data.snapLinePath ? (index.h("path", { d: data.snapLinePath, fill: "none", style: {
29804
- stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
29805
- strokeWidth: data.lineStrokeWidth,
29806
- strokeDasharray: data.dashArray,
29807
- strokeLinecap: 'round',
29808
- } })) : (data.edgeX !== undefined &&
29809
- data.edgeY !== undefined && (index.h("line", { x1: data.edgeX, y1: data.edgeY, x2: data.centerX, y2: data.centerY, style: {
29810
- stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
29811
- strokeWidth: data.lineStrokeWidth,
29812
- strokeDasharray: data.dashArray,
29813
- strokeLinecap: 'round',
29814
- } }))), index.h("circle", { cx: data.centerX, cy: data.centerY, r: data.indicatorRadius, style: {
29815
- fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
29816
- stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
29817
- strokeWidth: data.indicatorStrokeWidth,
29818
- } }))));
29819
29838
  })()), this.core.store.state.isContextMenuVisible && (index.h("kritzel-context-menu", { class: "context-menu", ref: el => (this.contextMenuElement = el ?? null), items: this.core.store.state.contextMenuItems, objects: this.core.store.selectionGroup?.objects || [], style: {
29820
29839
  position: 'absolute',
29821
29840
  left: `${this.core.store.state.contextMenuX}px`,
@@ -31227,7 +31246,7 @@ const KritzelPortal = class {
31227
31246
  * This file is auto-generated by the version bump scripts.
31228
31247
  * Do not modify manually.
31229
31248
  */
31230
- const KRITZEL_VERSION = '0.3.23';
31249
+ const KRITZEL_VERSION = '0.3.25';
31231
31250
 
31232
31251
  const kritzelSettingsCss = () => `:host{display:contents}kritzel-dialog{--kritzel-dialog-body-padding:0;--kritzel-dialog-width-large:800px;--kritzel-dialog-height-large:500px}.footer-button{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.cancel-button{border:1px solid #ebebeb;background:#fff;color:inherit}.cancel-button:hover{background:#f5f5f5}.settings-content{padding:0}.settings-content h3{margin:0 0 16px 0;font-size:18px;font-weight:600;color:var(--kritzel-settings-content-heading-color, #333333)}.settings-content p{margin:0;font-size:14px;color:var(--kritzel-settings-content-text-color, #666666);line-height:1.5}.settings-group{display:flex;flex-direction:column;gap:24px}.settings-item{display:flex;flex-direction:column;gap:8px}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-label{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.settings-description{font-size:12px;color:var(--kritzel-settings-description-color, #888888);margin:0;line-height:1.4}.shortcuts-list{display:flex;flex-direction:column;gap:24px}.shortcuts-category{display:flex;flex-direction:column;gap:8px}.shortcuts-category-title{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.shortcuts-group{display:flex;flex-direction:column;gap:4px}.shortcut-item{display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-item-bg, rgba(0, 0, 0, 0.02))}.shortcut-label{font-size:14px;color:var(--kritzel-settings-content-text-color, #666666)}.shortcut-key{font-family:monospace;font-size:12px;padding:2px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-key-bg, #f0f0f0);color:var(--kritzel-settings-shortcut-key-color, #333333);border:1px solid var(--kritzel-settings-shortcut-key-border, #ddd)}`;
31233
31252
 
@@ -20863,11 +20863,27 @@ class KritzelSelectionGroup extends KritzelBaseObject {
20863
20863
  /**
20864
20864
  * Sets the isSelected flag to false for all objects in the selection group.
20865
20865
  * Used when clearing or changing the selection state.
20866
+ *
20867
+ * For KritzelGroup members, also clears the isSelected flag on all nested
20868
+ * descendants. A descendant can retain a lingering isSelected from the marquee
20869
+ * preview before it was resolved to its parent group during selection, which
20870
+ * would otherwise leave that object selected after the group is deselected and
20871
+ * cause the whole group to be re-selected on the next interaction.
20866
20872
  */
20867
20873
  deselectAllChildren() {
20868
20874
  const children = this.objects;
20875
+ const descendantIds = new Set();
20869
20876
  for (const obj of children) {
20870
20877
  obj.isSelected = false;
20878
+ if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
20879
+ KritzelGroup.collectDescendantIds(obj, descendantIds);
20880
+ }
20881
+ }
20882
+ if (descendantIds.size > 0) {
20883
+ const descendants = this._core.store.objects.filter(o => descendantIds.has(o.id));
20884
+ for (const descendant of descendants) {
20885
+ descendant.isSelected = false;
20886
+ }
20871
20887
  }
20872
20888
  }
20873
20889
  /**
@@ -23640,6 +23656,11 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
23640
23656
  // Resolve child objects to their parent groups
23641
23657
  const parentGroup = KritzelGroup.findParentGroup(this._core, object.id);
23642
23658
  const objectToSelect = parentGroup || object;
23659
+ // When the click resolves to a parent group, clear the child's own flag so
23660
+ // it does not linger as selected and re-select the group on the next interaction.
23661
+ if (parentGroup) {
23662
+ object.isSelected = false;
23663
+ }
23643
23664
  const existingGroup = this._core.store.selectionGroup;
23644
23665
  if (shiftKey && existingGroup) {
23645
23666
  existingGroup.addOrRemove(objectToSelect);
@@ -23684,8 +23705,16 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
23684
23705
  const resolvedObjects = new Map();
23685
23706
  for (const obj of selectedObjects) {
23686
23707
  const parentGroup = childToParentMap.get(obj.id);
23687
- const toSelect = parentGroup || obj;
23688
- resolvedObjects.set(toSelect.id, toSelect);
23708
+ if (parentGroup) {
23709
+ // The child was selected (e.g. via marquee preview) but selection is
23710
+ // represented by its parent group. Clear the child's flag so it does
23711
+ // not linger as selected and re-select the group on the next interaction.
23712
+ obj.isSelected = false;
23713
+ resolvedObjects.set(parentGroup.id, parentGroup);
23714
+ }
23715
+ else {
23716
+ resolvedObjects.set(obj.id, obj);
23717
+ }
23689
23718
  }
23690
23719
  const unrolledObjects = Array.from(resolvedObjects.values());
23691
23720
  const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
@@ -27939,7 +27968,10 @@ class KritzelAnchorManager {
27939
27968
  // Snap Detection
27940
27969
  // ============================================
27941
27970
  /**
27942
- * Finds the nearest object that can be snapped to within the snap threshold.
27971
+ * Finds an object that can be snapped to when the cursor is sufficiently close to its center.
27972
+ * Snap is only triggered when the cursor is inside the object's bounds AND within 1/5 of the
27973
+ * shorter side from the center, so snapping activates only as the user approaches the center
27974
+ * and releases quickly as they move away.
27943
27975
  * Returns null if no suitable snap target is found.
27944
27976
  *
27945
27977
  * @param worldX - X coordinate in world space
@@ -27968,7 +28000,15 @@ class KritzelAnchorManager {
27968
28000
  const polygon = object.rotatedPolygon;
27969
28001
  const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
27970
28002
  if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
27971
- // If inside, check if this object is "above" the current best target
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
27972
28012
  if (object.zIndex > highestZIndex) {
27973
28013
  highestZIndex = object.zIndex;
27974
28014
  bestTarget = {
@@ -404,6 +404,11 @@ export class KritzelSelectionHandler extends KritzelBaseHandler {
404
404
  // Resolve child objects to their parent groups
405
405
  const parentGroup = KritzelGroup.findParentGroup(this._core, object.id);
406
406
  const objectToSelect = parentGroup || object;
407
+ // When the click resolves to a parent group, clear the child's own flag so
408
+ // it does not linger as selected and re-select the group on the next interaction.
409
+ if (parentGroup) {
410
+ object.isSelected = false;
411
+ }
407
412
  const existingGroup = this._core.store.selectionGroup;
408
413
  if (shiftKey && existingGroup) {
409
414
  existingGroup.addOrRemove(objectToSelect);
@@ -448,8 +453,16 @@ export class KritzelSelectionHandler extends KritzelBaseHandler {
448
453
  const resolvedObjects = new Map();
449
454
  for (const obj of selectedObjects) {
450
455
  const parentGroup = childToParentMap.get(obj.id);
451
- const toSelect = parentGroup || obj;
452
- resolvedObjects.set(toSelect.id, toSelect);
456
+ if (parentGroup) {
457
+ // The child was selected (e.g. via marquee preview) but selection is
458
+ // represented by its parent group. Clear the child's flag so it does
459
+ // not linger as selected and re-select the group on the next interaction.
460
+ obj.isSelected = false;
461
+ resolvedObjects.set(parentGroup.id, parentGroup);
462
+ }
463
+ else {
464
+ resolvedObjects.set(obj.id, obj);
465
+ }
453
466
  }
454
467
  const unrolledObjects = Array.from(resolvedObjects.values());
455
468
  const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
@@ -199,7 +199,10 @@ export class KritzelAnchorManager {
199
199
  // Snap Detection
200
200
  // ============================================
201
201
  /**
202
- * Finds the nearest object that can be snapped to within the snap threshold.
202
+ * Finds an object that can be snapped to when the cursor is sufficiently close to its center.
203
+ * Snap is only triggered when the cursor is inside the object's bounds AND within 1/5 of the
204
+ * shorter side from the center, so snapping activates only as the user approaches the center
205
+ * and releases quickly as they move away.
203
206
  * Returns null if no suitable snap target is found.
204
207
  *
205
208
  * @param worldX - X coordinate in world space
@@ -228,7 +231,15 @@ export class KritzelAnchorManager {
228
231
  const polygon = object.rotatedPolygon;
229
232
  const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
230
233
  if (KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
231
- // If inside, check if this object is "above" the current best target
234
+ // Only snap when the cursor is near the object's center (within 1/5 of the shorter side)
235
+ const snapRadius = Math.min(object.width, object.height) / 5;
236
+ const dx = worldX - object.centerX;
237
+ const dy = worldY - object.centerY;
238
+ const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
239
+ if (distanceToCenter > snapRadius) {
240
+ continue;
241
+ }
242
+ // If inside and close enough to center, check if this object is "above" the current best target
232
243
  if (object.zIndex > highestZIndex) {
233
244
  highestZIndex = object.zIndex;
234
245
  bestTarget = {
@@ -103,6 +103,7 @@ export class KritzelClipboardManager {
103
103
  */
104
104
  regenerateIdsAndRemapAnchors(topLevel) {
105
105
  const all = [];
106
+ const hasOnlyTopLevelLines = topLevel.length > 0 && topLevel.every(object => KritzelClassHelper.isInstanceOf(object, 'KritzelLine'));
106
107
  const collect = (objects) => {
107
108
  for (const object of objects) {
108
109
  all.push(object);
@@ -127,9 +128,15 @@ export class KritzelClipboardManager {
127
128
  if (object.startAnchor && idMap.has(object.startAnchor.objectId)) {
128
129
  object.startAnchor = { objectId: idMap.get(object.startAnchor.objectId) };
129
130
  }
131
+ else if (hasOnlyTopLevelLines) {
132
+ object.startAnchor = undefined;
133
+ }
130
134
  if (object.endAnchor && idMap.has(object.endAnchor.objectId)) {
131
135
  object.endAnchor = { objectId: idMap.get(object.endAnchor.objectId) };
132
136
  }
137
+ else if (hasOnlyTopLevelLines) {
138
+ object.endAnchor = undefined;
139
+ }
133
140
  }
134
141
  }
135
142
  }
@@ -1,5 +1,6 @@
1
1
  import { KritzelGeometryHelper } from "../../helpers/geometry.helper";
2
2
  import { KritzelBaseObject } from "./base-object.class";
3
+ import { KritzelGroup } from "./group.class";
3
4
  import { KritzelClassHelper } from "../../helpers/class.helper";
4
5
  import { KritzelMathHelper } from "../../helpers/math.helper";
5
6
  /**
@@ -197,11 +198,27 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
197
198
  /**
198
199
  * Sets the isSelected flag to false for all objects in the selection group.
199
200
  * Used when clearing or changing the selection state.
201
+ *
202
+ * For KritzelGroup members, also clears the isSelected flag on all nested
203
+ * descendants. A descendant can retain a lingering isSelected from the marquee
204
+ * preview before it was resolved to its parent group during selection, which
205
+ * would otherwise leave that object selected after the group is deselected and
206
+ * cause the whole group to be re-selected on the next interaction.
200
207
  */
201
208
  deselectAllChildren() {
202
209
  const children = this.objects;
210
+ const descendantIds = new Set();
203
211
  for (const obj of children) {
204
212
  obj.isSelected = false;
213
+ if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
214
+ KritzelGroup.collectDescendantIds(obj, descendantIds);
215
+ }
216
+ }
217
+ if (descendantIds.size > 0) {
218
+ const descendants = this._core.store.objects.filter(o => descendantIds.has(o.id));
219
+ for (const descendant of descendants) {
220
+ descendant.isSelected = false;
221
+ }
205
222
  }
206
223
  }
207
224
  /**
@@ -2155,7 +2155,11 @@ export class KritzelEngine {
2155
2155
  transform: object?.transformationMatrix,
2156
2156
  transformOrigin: 'top left',
2157
2157
  position: 'absolute',
2158
- zIndex: object.zIndex.toString(),
2158
+ // The object wrapper establishes its own stacking context (transform + z-index),
2159
+ // so the selection-line handles inside it cannot escape above the snap/anchor
2160
+ // layers (z-index 9999/9998) rendered later at the origin level. Lift the
2161
+ // selected line's wrapper above those layers so its handles always paint on top.
2162
+ zIndex: this.core.displaySelectionLineUI(object) ? '10001' : object.zIndex.toString(),
2159
2163
  pointerEvents: getObjectWrapperPointerEvents(object),
2160
2164
  } }, KritzelClassHelper.isInstanceOf(object, 'KritzelPath') && (h("svg", { ref: el => el && object.mount(el), xmlns: "http://www.w3.org/2000/svg", style: {
2161
2165
  height: object?.totalHeight + 'px',
@@ -2359,10 +2363,19 @@ export class KritzelEngine {
2359
2363
  }
2360
2364
  const selectionBorderColor = remoteUserColor ?? (KritzelColorHelper.resolveThemeColor(object.borderColor, currentTheme) || 'var(--kritzel-selection-border-color, #007AFF)');
2361
2365
  const selectionHandleStrokeColor = remoteUserColor ?? 'var(--kritzel-selection-handle-stroke-color, #007AFF)';
2366
+ const lineObject = object;
2367
+ const activeSnapCandidate = this.core.store.state.snapCandidate;
2368
+ const isStartHandleSnapped = lineObject.startAnchor != null;
2369
+ const isEndHandleSnapped = lineObject.endAnchor != null;
2370
+ const isStartHandleSnapPreview = activeSnapCandidate?.endpoint === 'start';
2371
+ const isEndHandleSnapPreview = activeSnapCandidate?.endpoint === 'end';
2372
+ const isStartHandleHighlighted = isStartHandleSnapped || isStartHandleSnapPreview;
2373
+ const isEndHandleHighlighted = isEndHandleSnapped || isEndHandleSnapPreview;
2374
+ const selectionOverlayZIndex = this.core.displaySelectionLineUI(object) ? '10000' : (object.zIndex + 1).toString();
2362
2375
  const handleScale = object.scale ?? 1;
2363
2376
  const { canRenderResizeHandles, canRenderRotationHandle } = this.getSelectionHandleCapabilities(object);
2364
2377
  return (h("svg", { xmlns: "http://www.w3.org/2000/svg", style: {
2365
- zIndex: (object.zIndex + 1).toString(),
2378
+ zIndex: selectionOverlayZIndex,
2366
2379
  height: object?.totalHeight.toString(),
2367
2380
  width: object?.totalWidth.toString(),
2368
2381
  left: '0',
@@ -2453,7 +2466,7 @@ export class KritzelEngine {
2453
2466
  strokeLinecap: 'round',
2454
2467
  fill: 'none',
2455
2468
  } })), !this.isSelecting && !isRemoteSelection && (h("g", { class: "selection-line-handles", style: { pointerEvents: 'auto' } }, h("circle", { class: "selection-line-handle start", cx: object.startX - object.x, cy: object.startY - object.y, r: `${((baseHandleSize - 1) * object.scale) / this.core.store.state?.scale}`, style: {
2456
- fill: 'var(--kritzel-selection-handle-color, #000000)',
2469
+ fill: isStartHandleHighlighted ? selectionHandleStrokeColor : 'var(--kritzel-selection-handle-color, #000000)',
2457
2470
  stroke: selectionHandleStrokeColor,
2458
2471
  strokeWidth: `calc(2px * ${object.scale} / ${this.core.store.state?.scale})`,
2459
2472
  paintOrder: 'fill',
@@ -2477,7 +2490,7 @@ export class KritzelEngine {
2477
2490
  fill: 'transparent',
2478
2491
  paintOrder: 'fill',
2479
2492
  } }), h("circle", { class: "selection-line-handle end", cx: object.endX - object.x, cy: object.endY - object.y, r: `${((baseHandleSize - 1) * object.scale) / this.core.store.state?.scale}`, style: {
2480
- fill: 'var(--kritzel-selection-handle-color, #000000)',
2493
+ fill: isEndHandleHighlighted ? selectionHandleStrokeColor : 'var(--kritzel-selection-handle-color, #000000)',
2481
2494
  stroke: selectionHandleStrokeColor,
2482
2495
  strokeWidth: `calc(2px * ${object.scale} / ${this.core.store.state?.scale})`,
2483
2496
  paintOrder: 'fill',
@@ -2487,6 +2500,34 @@ export class KritzelEngine {
2487
2500
  } })))))));
2488
2501
  })()));
2489
2502
  }), (() => {
2503
+ const data = this.core.anchorManager.getSnapIndicatorRenderData();
2504
+ if (!data)
2505
+ return null;
2506
+ return (h("svg", { xmlns: "http://www.w3.org/2000/svg", class: "snap-indicator", style: {
2507
+ position: 'absolute',
2508
+ left: '0',
2509
+ top: '0',
2510
+ width: '1px',
2511
+ height: '1px',
2512
+ pointerEvents: 'none',
2513
+ zIndex: '9999',
2514
+ overflow: 'visible',
2515
+ } }, h("g", null, data.snapLinePath ? (h("path", { d: data.snapLinePath, fill: "none", style: {
2516
+ stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
2517
+ strokeWidth: data.lineStrokeWidth,
2518
+ strokeDasharray: data.dashArray,
2519
+ strokeLinecap: 'round',
2520
+ } })) : (data.edgeX !== undefined &&
2521
+ data.edgeY !== undefined && (h("line", { x1: data.edgeX, y1: data.edgeY, x2: data.centerX, y2: data.centerY, style: {
2522
+ stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
2523
+ strokeWidth: data.lineStrokeWidth,
2524
+ strokeDasharray: data.dashArray,
2525
+ strokeLinecap: 'round',
2526
+ } }))), h("circle", { cx: data.centerX, cy: data.centerY, r: data.indicatorRadius, style: {
2527
+ fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
2528
+ strokeWidth: data.indicatorStrokeWidth,
2529
+ } }))));
2530
+ })(), (() => {
2490
2531
  const data = this.core.anchorManager.getAnchorLinesRenderData();
2491
2532
  if (!data)
2492
2533
  return null;
@@ -2511,7 +2552,7 @@ export class KritzelEngine {
2511
2552
  strokeDasharray: data.dashArray,
2512
2553
  strokeLinecap: 'round',
2513
2554
  } })), h("circle", { cx: data.startAnchorViz.centerX, cy: data.startAnchorViz.centerY, r: data.indicatorRadius, style: {
2514
- fill: 'var(--kritzel-snap-indicator-fill, rgba(0, 0, 0))',
2555
+ fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
2515
2556
  stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
2516
2557
  strokeWidth: data.indicatorStrokeWidth,
2517
2558
  } }))), data.endAnchorViz && (h("g", { class: "anchor-line-end" }, data.endAnchorViz.pathD ? (h("path", { d: data.endAnchorViz.pathD, style: {
@@ -2530,35 +2571,6 @@ export class KritzelEngine {
2530
2571
  stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
2531
2572
  strokeWidth: data.indicatorStrokeWidth,
2532
2573
  } })))));
2533
- })(), (() => {
2534
- const data = this.core.anchorManager.getSnapIndicatorRenderData();
2535
- if (!data)
2536
- return null;
2537
- return (h("svg", { xmlns: "http://www.w3.org/2000/svg", class: "snap-indicator", style: {
2538
- position: 'absolute',
2539
- left: '0',
2540
- top: '0',
2541
- width: '1px',
2542
- height: '1px',
2543
- pointerEvents: 'none',
2544
- zIndex: '9999',
2545
- overflow: 'visible',
2546
- } }, h("g", null, data.snapLinePath ? (h("path", { d: data.snapLinePath, fill: "none", style: {
2547
- stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
2548
- strokeWidth: data.lineStrokeWidth,
2549
- strokeDasharray: data.dashArray,
2550
- strokeLinecap: 'round',
2551
- } })) : (data.edgeX !== undefined &&
2552
- data.edgeY !== undefined && (h("line", { x1: data.edgeX, y1: data.edgeY, x2: data.centerX, y2: data.centerY, style: {
2553
- stroke: 'var(--kritzel-snap-line-stroke, rgba(0, 0, 0, 0.2))',
2554
- strokeWidth: data.lineStrokeWidth,
2555
- strokeDasharray: data.dashArray,
2556
- strokeLinecap: 'round',
2557
- } }))), h("circle", { cx: data.centerX, cy: data.centerY, r: data.indicatorRadius, style: {
2558
- fill: 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))',
2559
- stroke: 'var(--kritzel-snap-indicator-stroke, #007bff)',
2560
- strokeWidth: data.indicatorStrokeWidth,
2561
- } }))));
2562
2574
  })()), this.core.store.state.isContextMenuVisible && (h("kritzel-context-menu", { class: "context-menu", ref: el => (this.contextMenuElement = el ?? null), items: this.core.store.state.contextMenuItems, objects: this.core.store.selectionGroup?.objects || [], style: {
2563
2575
  position: 'absolute',
2564
2576
  left: `${this.core.store.state.contextMenuX}px`,
@@ -3,4 +3,4 @@
3
3
  * This file is auto-generated by the version bump scripts.
4
4
  * Do not modify manually.
5
5
  */
6
- export const KRITZEL_VERSION = '0.3.23';
6
+ export const KRITZEL_VERSION = '0.3.25';