kritzel-stencil 0.2.3 → 0.2.4

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 (38) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +55 -7
  3. package/dist/cjs/{workspace.migrations-CYeB_XRB.js → workspace.migrations-B89-6fP-.js} +34 -8
  4. package/dist/collection/classes/core/core.class.js +9 -2
  5. package/dist/collection/classes/handlers/line-handle.handler.js +1 -0
  6. package/dist/collection/classes/handlers/move.handler.js +2 -0
  7. package/dist/collection/classes/handlers/resize.handler.js +2 -0
  8. package/dist/collection/classes/handlers/rotation.handler.js +2 -0
  9. package/dist/collection/classes/structures/object-map.structure.js +44 -3
  10. package/dist/collection/classes/tools/brush-tool.class.js +2 -0
  11. package/dist/collection/classes/tools/eraser-tool.class.js +22 -8
  12. package/dist/collection/classes/tools/line-tool.class.js +2 -0
  13. package/dist/collection/classes/tools/shape-tool.class.js +1 -0
  14. package/dist/collection/constants/version.js +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/kritzel-controls.js +1 -1
  17. package/dist/components/kritzel-editor.js +1 -1
  18. package/dist/components/kritzel-engine.js +1 -1
  19. package/dist/components/kritzel-settings.js +1 -1
  20. package/dist/components/kritzel-tool-config.js +1 -1
  21. package/dist/components/{p-yX5Zk5pS.js → p-3YivOJM2.js} +1 -1
  22. package/dist/components/p-B2dVTxsc.js +9 -0
  23. package/dist/components/{p-CdaOQi45.js → p-BabNumqA.js} +1 -1
  24. package/dist/components/{p-BeeKeeeo.js → p-DMYfjC1C.js} +1 -1
  25. package/dist/components/p-Dqpa31TI.js +1 -0
  26. package/dist/esm/index.js +2 -2
  27. package/dist/esm/kritzel-active-users_42.entry.js +55 -7
  28. package/dist/esm/{workspace.migrations-BrA5xRPn.js → workspace.migrations-D_y5zlxK.js} +34 -8
  29. package/dist/stencil/index.esm.js +1 -1
  30. package/dist/stencil/p-74898384.entry.js +9 -0
  31. package/dist/stencil/{p-BrA5xRPn.js → p-D_y5zlxK.js} +1 -1
  32. package/dist/stencil/stencil.esm.js +1 -1
  33. package/dist/types/classes/structures/object-map.structure.d.ts +19 -0
  34. package/dist/types/constants/version.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/dist/components/p-Bs7lEBy5.js +0 -9
  37. package/dist/components/p-C1UNiqO2.js +0 -1
  38. package/dist/stencil/p-56b81681.entry.js +0 -9
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var workspace_migrations = require('./workspace.migrations-CYeB_XRB.js');
3
+ var workspace_migrations = require('./workspace.migrations-B89-6fP-.js');
4
4
  var Y = require('yjs');
5
5
  var yWebsocket = require('y-websocket');
6
6
  require('y-indexeddb');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var index = require('./index-CFnj_FXt.js');
4
- var workspace_migrations = require('./workspace.migrations-CYeB_XRB.js');
4
+ var workspace_migrations = require('./workspace.migrations-B89-6fP-.js');
5
5
  var Y = require('yjs');
6
6
  require('y-indexeddb');
7
7
  require('y-websocket');
@@ -21445,6 +21445,12 @@ class KritzelObjectMap {
21445
21445
  * redundant full serializations of every child object per frame.
21446
21446
  */
21447
21447
  _localOnlyMode = false;
21448
+ /**
21449
+ * Tracks whether we're currently executing inside a `transaction()`
21450
+ * callback. While true, `markUndoBoundary()` is a no-op so nested
21451
+ * insert/remove calls don't split the in-progress undo step.
21452
+ */
21453
+ _inTransaction = false;
21448
21454
  /**
21449
21455
  * Indicates whether the object map has been initialized and is ready for use.
21450
21456
  * @returns `true` if providers are connected and the map is operational
@@ -21683,9 +21689,12 @@ class KritzelObjectMap {
21683
21689
  }
21684
21690
  this._providers.push(provider);
21685
21691
  }
21686
- // Set up undo/redo manager for this workspace
21692
+ // captureTimeout is effectively infinite undo-step boundaries are
21693
+ // marked explicitly via markUndoBoundary() in insert/remove/reset and
21694
+ // at stroke/gesture boundaries in the tools. This prevents a single
21695
+ // brush stroke from being split when the user pauses after pointerdown.
21687
21696
  this._undoManager = new Y__namespace.UndoManager([this._objectsMap], {
21688
- captureTimeout: 200,
21697
+ captureTimeout: Number.MAX_SAFE_INTEGER,
21689
21698
  trackedOrigins: new Set(['local', 'temporary']),
21690
21699
  ignoreRemoteMapChanges: true,
21691
21700
  });
@@ -22003,8 +22012,34 @@ class KritzelObjectMap {
22003
22012
  */
22004
22013
  transaction(callback) {
22005
22014
  if (this._ydoc) {
22006
- this._ydoc.transact(callback, 'local');
22015
+ this._inTransaction = true;
22016
+ try {
22017
+ this._ydoc.transact(callback, 'local');
22018
+ }
22019
+ finally {
22020
+ this._inTransaction = false;
22021
+ }
22022
+ }
22023
+ }
22024
+ /**
22025
+ * Closes the current undo stack item so the next tracked change starts
22026
+ * a new one. Call at gesture/stroke boundaries (e.g. drag/resize/rotate
22027
+ * end, brush/line/shape stroke end).
22028
+ *
22029
+ * No-op while a transaction() is open.
22030
+ */
22031
+ stopUndoCapturing() {
22032
+ this.markUndoBoundary();
22033
+ }
22034
+ /**
22035
+ * Internal: closes the current undo stack item. No-op while inside a
22036
+ * transaction() so wrapped multi-op work stays a single undo step.
22037
+ */
22038
+ markUndoBoundary() {
22039
+ if (this._inTransaction) {
22040
+ return;
22007
22041
  }
22042
+ this._undoManager?.stopCapturing();
22008
22043
  }
22009
22044
  /**
22010
22045
  * Executes a callback where all update() calls only modify local state
@@ -22069,6 +22104,7 @@ class KritzelObjectMap {
22069
22104
  reset() {
22070
22105
  this.quadtree.reset();
22071
22106
  this._idMap.clear();
22107
+ this.markUndoBoundary();
22072
22108
  this._ydoc?.transact(() => {
22073
22109
  this._objectsMap?.clear();
22074
22110
  }, 'local');
@@ -22089,6 +22125,7 @@ class KritzelObjectMap {
22089
22125
  this._idMap.set(object.id, object);
22090
22126
  if (this._objectsMap && this.isPersistable(object)) {
22091
22127
  const serialized = object.serialize();
22128
+ this.markUndoBoundary();
22092
22129
  this._ydoc?.transact(() => {
22093
22130
  this._objectsMap?.set(object.id, serialized);
22094
22131
  }, 'local');
@@ -22133,6 +22170,10 @@ class KritzelObjectMap {
22133
22170
  */
22134
22171
  remove(predicate) {
22135
22172
  const objectsToRemove = this.quadtree.filter(predicate);
22173
+ if (objectsToRemove.length === 0) {
22174
+ return;
22175
+ }
22176
+ this.markUndoBoundary();
22136
22177
  for (const object of objectsToRemove) {
22137
22178
  this.quadtree.remove(o => o.id === object.id);
22138
22179
  this._idMap.delete(object.id);
@@ -23823,8 +23864,15 @@ class KritzelCore {
23823
23864
  if (!selectionGroup) {
23824
23865
  return;
23825
23866
  }
23826
- selectionGroup.objects.forEach(obj => this.removeObject(obj));
23827
- this.removeSelectionGroup();
23867
+ // Seal the selection group's creation into its own undo step so Yjs
23868
+ // doesn't collapse create+delete of the SG into a no-op on undo.
23869
+ this._store.objects.stopUndoCapturing();
23870
+ this._store.objects.transaction(() => {
23871
+ selectionGroup.objects.forEach(obj => this.removeObject(obj));
23872
+ this.removeSelectionGroup();
23873
+ });
23874
+ // Close the deletion step so subsequent unrelated changes start a new step.
23875
+ this._store.objects.stopUndoCapturing();
23828
23876
  this.engine.emitObjectsInViewportChange();
23829
23877
  this.rerender();
23830
23878
  }
@@ -29041,7 +29089,7 @@ const KritzelPortal = class {
29041
29089
  * This file is auto-generated by the version bump scripts.
29042
29090
  * Do not modify manually.
29043
29091
  */
29044
- const KRITZEL_VERSION = '0.2.3';
29092
+ const KRITZEL_VERSION = '0.2.4';
29045
29093
 
29046
29094
  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)}`;
29047
29095
 
@@ -19679,6 +19679,7 @@ class KritzelBrushTool extends KritzelBaseTool {
19679
19679
  }
19680
19680
  this._core.store.objects?.setActiveDrawingObject(null);
19681
19681
  this._currentPathId = null;
19682
+ this._core.store.objects?.stopUndoCapturing();
19682
19683
  }
19683
19684
  }
19684
19685
  }
@@ -19695,6 +19696,7 @@ class KritzelBrushTool extends KritzelBaseTool {
19695
19696
  }
19696
19697
  this._core.store.objects?.setActiveDrawingObject(null);
19697
19698
  this._currentPathId = null;
19699
+ this._core.store.objects?.stopUndoCapturing();
19698
19700
  }
19699
19701
  }
19700
19702
  }
@@ -20526,6 +20528,7 @@ class KritzelLineTool extends KritzelBaseTool {
20526
20528
  }
20527
20529
  this._core.store.objects?.setActiveDrawingObject(null);
20528
20530
  this._currentLineId = null;
20531
+ this._core.store.objects?.stopUndoCapturing();
20529
20532
  }
20530
20533
  }
20531
20534
  }
@@ -20544,6 +20547,7 @@ class KritzelLineTool extends KritzelBaseTool {
20544
20547
  }
20545
20548
  this._core.store.objects?.setActiveDrawingObject(null);
20546
20549
  this._currentLineId = null;
20550
+ this._core.store.objects?.stopUndoCapturing();
20547
20551
  }
20548
20552
  }
20549
20553
  }
@@ -20650,17 +20654,24 @@ class KritzelEraserTool extends KritzelBaseTool {
20650
20654
  if (event.pointerType === 'mouse') {
20651
20655
  if (this._core.store.state.isErasing) {
20652
20656
  const objectsToRemove = this._core.store.allObjects.filter(object => object.markedForRemoval);
20653
- objectsToRemove.forEach(object => {
20654
- object.markedForRemoval = false;
20655
- this._core.removeObject(object);
20656
- });
20657
20657
  if (objectsToRemove.length > 0) {
20658
+ // Seal the previous undo step so create+delete of the same object
20659
+ // (e.g. just-drawn shape) don't collapse into a no-op on undo.
20660
+ this._core.store.objects.stopUndoCapturing();
20661
+ // Group all removals from this sweep into a single undo step.
20662
+ this._core.store.objects.transaction(() => {
20663
+ objectsToRemove.forEach(object => {
20664
+ object.markedForRemoval = false;
20665
+ this._core.removeObject(object);
20666
+ });
20667
+ });
20658
20668
  this._core.rerender();
20659
20669
  }
20660
20670
  this._core.store.state.isErasing = false;
20661
20671
  this._core.engine.emitObjectsChange();
20662
20672
  if (objectsToRemove.length > 0) {
20663
20673
  this._core.engine.emitObjectsRemoved(objectsToRemove);
20674
+ this._core.store.objects.stopUndoCapturing();
20664
20675
  }
20665
20676
  }
20666
20677
  }
@@ -20668,17 +20679,24 @@ class KritzelEraserTool extends KritzelBaseTool {
20668
20679
  clearTimeout(this.touchStartTimeout);
20669
20680
  if (this._core.store.state.isErasing) {
20670
20681
  const objectsToRemove = this._core.store.allObjects.filter(object => object.markedForRemoval);
20671
- objectsToRemove.forEach(object => {
20672
- object.markedForRemoval = false;
20673
- this._core.removeObject(object);
20674
- });
20675
20682
  if (objectsToRemove.length > 0) {
20683
+ // Seal the previous undo step so create+delete of the same object
20684
+ // (e.g. just-drawn shape) don't collapse into a no-op on undo.
20685
+ this._core.store.objects.stopUndoCapturing();
20686
+ // Group all removals from this sweep into a single undo step.
20687
+ this._core.store.objects.transaction(() => {
20688
+ objectsToRemove.forEach(object => {
20689
+ object.markedForRemoval = false;
20690
+ this._core.removeObject(object);
20691
+ });
20692
+ });
20676
20693
  this._core.rerender();
20677
20694
  }
20678
20695
  this._core.store.state.isErasing = false;
20679
20696
  this._core.engine.emitObjectsChange();
20680
20697
  if (objectsToRemove.length > 0) {
20681
20698
  this._core.engine.emitObjectsRemoved(objectsToRemove);
20699
+ this._core.store.objects.stopUndoCapturing();
20682
20700
  }
20683
20701
  }
20684
20702
  }
@@ -21283,6 +21301,7 @@ class KritzelShapeTool extends KritzelBaseTool {
21283
21301
  this.isDrawing = false;
21284
21302
  this._core.store.objects?.setActiveDrawingObject(null);
21285
21303
  this.currentShape = null;
21304
+ this._core.store.objects?.stopUndoCapturing();
21286
21305
  this._core.rerender();
21287
21306
  }
21288
21307
  }
@@ -21692,6 +21711,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
21692
21711
  this._core.store.selectionGroup.update();
21693
21712
  this._core.engine.emitObjectsChange();
21694
21713
  this._core.store.state.hasObjectsChanged = true;
21714
+ this._core.store.objects.stopUndoCapturing();
21695
21715
  }
21696
21716
  }
21697
21717
  }
@@ -21703,6 +21723,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
21703
21723
  this._core.store.selectionGroup.update();
21704
21724
  this._core.engine.emitObjectsChange();
21705
21725
  this._core.store.state.hasObjectsChanged = true;
21726
+ this._core.store.objects.stopUndoCapturing();
21706
21727
  }
21707
21728
  }
21708
21729
  }
@@ -22055,6 +22076,7 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22055
22076
  this._core.store.selectionGroup.update();
22056
22077
  this._core.engine.emitObjectsChange();
22057
22078
  this._core.store.state.hasObjectsChanged = true;
22079
+ this._core.store.objects.stopUndoCapturing();
22058
22080
  }
22059
22081
  this.reset();
22060
22082
  }
@@ -22066,6 +22088,7 @@ class KritzelResizeHandler extends KritzelBaseHandler {
22066
22088
  this._core.store.selectionGroup.update();
22067
22089
  this._core.engine.emitObjectsChange();
22068
22090
  this._core.store.state.hasObjectsChanged = true;
22091
+ this._core.store.objects.stopUndoCapturing();
22069
22092
  }
22070
22093
  this.reset();
22071
22094
  const timeout = this._core.store.state.longTouchTimeout;
@@ -22217,6 +22240,7 @@ class KritzelRotationHandler extends KritzelBaseHandler {
22217
22240
  this._core.engine.emitObjectsChange();
22218
22241
  this._core.store.state.isRotating = false;
22219
22242
  this._core.store.state.hasObjectsChanged = true;
22243
+ this._core.store.objects.stopUndoCapturing();
22220
22244
  this.reset();
22221
22245
  }
22222
22246
  }
@@ -22226,6 +22250,7 @@ class KritzelRotationHandler extends KritzelBaseHandler {
22226
22250
  this._core.engine.emitObjectsChange();
22227
22251
  this._core.store.state.isRotating = false;
22228
22252
  this._core.store.state.hasObjectsChanged = true;
22253
+ this._core.store.objects.stopUndoCapturing();
22229
22254
  this.reset();
22230
22255
  const timeout = this._core.store.state.longTouchTimeout;
22231
22256
  if (timeout) {
@@ -23243,6 +23268,7 @@ class KritzelLineHandleHandler extends KritzelBaseHandler {
23243
23268
  this._core.engine.emitObjectsChange();
23244
23269
  this._core.store.state.hasObjectsChanged = true;
23245
23270
  }
23271
+ this._core.store.objects.stopUndoCapturing();
23246
23272
  }
23247
23273
  this._core.store.state.isLineHandleDragging = false;
23248
23274
  this.reset();
@@ -587,8 +587,15 @@ export class KritzelCore {
587
587
  if (!selectionGroup) {
588
588
  return;
589
589
  }
590
- selectionGroup.objects.forEach(obj => this.removeObject(obj));
591
- this.removeSelectionGroup();
590
+ // Seal the selection group's creation into its own undo step so Yjs
591
+ // doesn't collapse create+delete of the SG into a no-op on undo.
592
+ this._store.objects.stopUndoCapturing();
593
+ this._store.objects.transaction(() => {
594
+ selectionGroup.objects.forEach(obj => this.removeObject(obj));
595
+ this.removeSelectionGroup();
596
+ });
597
+ // Close the deletion step so subsequent unrelated changes start a new step.
598
+ this._store.objects.stopUndoCapturing();
592
599
  this.engine.emitObjectsInViewportChange();
593
600
  this.rerender();
594
601
  }
@@ -439,6 +439,7 @@ export class KritzelLineHandleHandler extends KritzelBaseHandler {
439
439
  this._core.engine.emitObjectsChange();
440
440
  this._core.store.state.hasObjectsChanged = true;
441
441
  }
442
+ this._core.store.objects.stopUndoCapturing();
442
443
  }
443
444
  this._core.store.state.isLineHandleDragging = false;
444
445
  this.reset();
@@ -214,6 +214,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
214
214
  this._core.store.selectionGroup.update();
215
215
  this._core.engine.emitObjectsChange();
216
216
  this._core.store.state.hasObjectsChanged = true;
217
+ this._core.store.objects.stopUndoCapturing();
217
218
  }
218
219
  }
219
220
  }
@@ -225,6 +226,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
225
226
  this._core.store.selectionGroup.update();
226
227
  this._core.engine.emitObjectsChange();
227
228
  this._core.store.state.hasObjectsChanged = true;
229
+ this._core.store.objects.stopUndoCapturing();
228
230
  }
229
231
  }
230
232
  }
@@ -280,6 +280,7 @@ export class KritzelResizeHandler extends KritzelBaseHandler {
280
280
  this._core.store.selectionGroup.update();
281
281
  this._core.engine.emitObjectsChange();
282
282
  this._core.store.state.hasObjectsChanged = true;
283
+ this._core.store.objects.stopUndoCapturing();
283
284
  }
284
285
  this.reset();
285
286
  }
@@ -291,6 +292,7 @@ export class KritzelResizeHandler extends KritzelBaseHandler {
291
292
  this._core.store.selectionGroup.update();
292
293
  this._core.engine.emitObjectsChange();
293
294
  this._core.store.state.hasObjectsChanged = true;
295
+ this._core.store.objects.stopUndoCapturing();
294
296
  }
295
297
  this.reset();
296
298
  const timeout = this._core.store.state.longTouchTimeout;
@@ -140,6 +140,7 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
140
140
  this._core.engine.emitObjectsChange();
141
141
  this._core.store.state.isRotating = false;
142
142
  this._core.store.state.hasObjectsChanged = true;
143
+ this._core.store.objects.stopUndoCapturing();
143
144
  this.reset();
144
145
  }
145
146
  }
@@ -149,6 +150,7 @@ export class KritzelRotationHandler extends KritzelBaseHandler {
149
150
  this._core.engine.emitObjectsChange();
150
151
  this._core.store.state.isRotating = false;
151
152
  this._core.store.state.hasObjectsChanged = true;
153
+ this._core.store.objects.stopUndoCapturing();
152
154
  this.reset();
153
155
  const timeout = this._core.store.state.longTouchTimeout;
154
156
  if (timeout) {
@@ -44,6 +44,12 @@ export class KritzelObjectMap {
44
44
  * redundant full serializations of every child object per frame.
45
45
  */
46
46
  _localOnlyMode = false;
47
+ /**
48
+ * Tracks whether we're currently executing inside a `transaction()`
49
+ * callback. While true, `markUndoBoundary()` is a no-op so nested
50
+ * insert/remove calls don't split the in-progress undo step.
51
+ */
52
+ _inTransaction = false;
47
53
  /**
48
54
  * Indicates whether the object map has been initialized and is ready for use.
49
55
  * @returns `true` if providers are connected and the map is operational
@@ -282,9 +288,12 @@ export class KritzelObjectMap {
282
288
  }
283
289
  this._providers.push(provider);
284
290
  }
285
- // Set up undo/redo manager for this workspace
291
+ // captureTimeout is effectively infinite undo-step boundaries are
292
+ // marked explicitly via markUndoBoundary() in insert/remove/reset and
293
+ // at stroke/gesture boundaries in the tools. This prevents a single
294
+ // brush stroke from being split when the user pauses after pointerdown.
286
295
  this._undoManager = new Y.UndoManager([this._objectsMap], {
287
- captureTimeout: 200,
296
+ captureTimeout: Number.MAX_SAFE_INTEGER,
288
297
  trackedOrigins: new Set(['local', 'temporary']),
289
298
  ignoreRemoteMapChanges: true,
290
299
  });
@@ -602,9 +611,35 @@ export class KritzelObjectMap {
602
611
  */
603
612
  transaction(callback) {
604
613
  if (this._ydoc) {
605
- this._ydoc.transact(callback, 'local');
614
+ this._inTransaction = true;
615
+ try {
616
+ this._ydoc.transact(callback, 'local');
617
+ }
618
+ finally {
619
+ this._inTransaction = false;
620
+ }
606
621
  }
607
622
  }
623
+ /**
624
+ * Closes the current undo stack item so the next tracked change starts
625
+ * a new one. Call at gesture/stroke boundaries (e.g. drag/resize/rotate
626
+ * end, brush/line/shape stroke end).
627
+ *
628
+ * No-op while a transaction() is open.
629
+ */
630
+ stopUndoCapturing() {
631
+ this.markUndoBoundary();
632
+ }
633
+ /**
634
+ * Internal: closes the current undo stack item. No-op while inside a
635
+ * transaction() so wrapped multi-op work stays a single undo step.
636
+ */
637
+ markUndoBoundary() {
638
+ if (this._inTransaction) {
639
+ return;
640
+ }
641
+ this._undoManager?.stopCapturing();
642
+ }
608
643
  /**
609
644
  * Executes a callback where all update() calls only modify local state
610
645
  * (quadtree + idMap) without writing to Yjs. This avoids expensive full
@@ -668,6 +703,7 @@ export class KritzelObjectMap {
668
703
  reset() {
669
704
  this.quadtree.reset();
670
705
  this._idMap.clear();
706
+ this.markUndoBoundary();
671
707
  this._ydoc?.transact(() => {
672
708
  this._objectsMap?.clear();
673
709
  }, 'local');
@@ -688,6 +724,7 @@ export class KritzelObjectMap {
688
724
  this._idMap.set(object.id, object);
689
725
  if (this._objectsMap && this.isPersistable(object)) {
690
726
  const serialized = object.serialize();
727
+ this.markUndoBoundary();
691
728
  this._ydoc?.transact(() => {
692
729
  this._objectsMap?.set(object.id, serialized);
693
730
  }, 'local');
@@ -732,6 +769,10 @@ export class KritzelObjectMap {
732
769
  */
733
770
  remove(predicate) {
734
771
  const objectsToRemove = this.quadtree.filter(predicate);
772
+ if (objectsToRemove.length === 0) {
773
+ return;
774
+ }
775
+ this.markUndoBoundary();
735
776
  for (const object of objectsToRemove) {
736
777
  this.quadtree.remove(o => o.id === object.id);
737
778
  this._idMap.delete(object.id);
@@ -172,6 +172,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
172
172
  }
173
173
  this._core.store.objects?.setActiveDrawingObject(null);
174
174
  this._currentPathId = null;
175
+ this._core.store.objects?.stopUndoCapturing();
175
176
  }
176
177
  }
177
178
  }
@@ -188,6 +189,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
188
189
  }
189
190
  this._core.store.objects?.setActiveDrawingObject(null);
190
191
  this._currentPathId = null;
192
+ this._core.store.objects?.stopUndoCapturing();
191
193
  }
192
194
  }
193
195
  }
@@ -82,17 +82,24 @@ export class KritzelEraserTool extends KritzelBaseTool {
82
82
  if (event.pointerType === 'mouse') {
83
83
  if (this._core.store.state.isErasing) {
84
84
  const objectsToRemove = this._core.store.allObjects.filter(object => object.markedForRemoval);
85
- objectsToRemove.forEach(object => {
86
- object.markedForRemoval = false;
87
- this._core.removeObject(object);
88
- });
89
85
  if (objectsToRemove.length > 0) {
86
+ // Seal the previous undo step so create+delete of the same object
87
+ // (e.g. just-drawn shape) don't collapse into a no-op on undo.
88
+ this._core.store.objects.stopUndoCapturing();
89
+ // Group all removals from this sweep into a single undo step.
90
+ this._core.store.objects.transaction(() => {
91
+ objectsToRemove.forEach(object => {
92
+ object.markedForRemoval = false;
93
+ this._core.removeObject(object);
94
+ });
95
+ });
90
96
  this._core.rerender();
91
97
  }
92
98
  this._core.store.state.isErasing = false;
93
99
  this._core.engine.emitObjectsChange();
94
100
  if (objectsToRemove.length > 0) {
95
101
  this._core.engine.emitObjectsRemoved(objectsToRemove);
102
+ this._core.store.objects.stopUndoCapturing();
96
103
  }
97
104
  }
98
105
  }
@@ -100,17 +107,24 @@ export class KritzelEraserTool extends KritzelBaseTool {
100
107
  clearTimeout(this.touchStartTimeout);
101
108
  if (this._core.store.state.isErasing) {
102
109
  const objectsToRemove = this._core.store.allObjects.filter(object => object.markedForRemoval);
103
- objectsToRemove.forEach(object => {
104
- object.markedForRemoval = false;
105
- this._core.removeObject(object);
106
- });
107
110
  if (objectsToRemove.length > 0) {
111
+ // Seal the previous undo step so create+delete of the same object
112
+ // (e.g. just-drawn shape) don't collapse into a no-op on undo.
113
+ this._core.store.objects.stopUndoCapturing();
114
+ // Group all removals from this sweep into a single undo step.
115
+ this._core.store.objects.transaction(() => {
116
+ objectsToRemove.forEach(object => {
117
+ object.markedForRemoval = false;
118
+ this._core.removeObject(object);
119
+ });
120
+ });
108
121
  this._core.rerender();
109
122
  }
110
123
  this._core.store.state.isErasing = false;
111
124
  this._core.engine.emitObjectsChange();
112
125
  if (objectsToRemove.length > 0) {
113
126
  this._core.engine.emitObjectsRemoved(objectsToRemove);
127
+ this._core.store.objects.stopUndoCapturing();
114
128
  }
115
129
  }
116
130
  }
@@ -201,6 +201,7 @@ export class KritzelLineTool extends KritzelBaseTool {
201
201
  }
202
202
  this._core.store.objects?.setActiveDrawingObject(null);
203
203
  this._currentLineId = null;
204
+ this._core.store.objects?.stopUndoCapturing();
204
205
  }
205
206
  }
206
207
  }
@@ -219,6 +220,7 @@ export class KritzelLineTool extends KritzelBaseTool {
219
220
  }
220
221
  this._core.store.objects?.setActiveDrawingObject(null);
221
222
  this._currentLineId = null;
223
+ this._core.store.objects?.stopUndoCapturing();
222
224
  }
223
225
  }
224
226
  }
@@ -242,6 +242,7 @@ export class KritzelShapeTool extends KritzelBaseTool {
242
242
  this.isDrawing = false;
243
243
  this._core.store.objects?.setActiveDrawingObject(null);
244
244
  this.currentShape = null;
245
+ this._core.store.objects?.stopUndoCapturing();
245
246
  this._core.rerender();
246
247
  }
247
248
  }
@@ -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.2.3';
6
+ export const KRITZEL_VERSION = '0.2.4';