kritzel-stencil 0.1.81 → 0.1.83

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 (35) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +28 -3
  3. package/dist/cjs/{workspace.migrations-BVBITEM5.js → workspace.migrations-K5oVASsb.js} +86 -58
  4. package/dist/collection/classes/handlers/move.handler.js +2 -0
  5. package/dist/collection/classes/objects/selection-group.class.js +47 -9
  6. package/dist/collection/classes/objects/shape.class.js +14 -20
  7. package/dist/collection/classes/objects/text.class.js +23 -29
  8. package/dist/collection/classes/structures/object-map.structure.js +26 -1
  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-CwQOwQm0.js → p-CKfjz1gj.js} +2 -2
  17. package/dist/components/{p-DXhiNQyH.js → p-CLo-TjD_.js} +1 -1
  18. package/dist/components/{p--7j0gU6F.js → p-CYl_JKvH.js} +1 -1
  19. package/dist/components/{p-Dxr5oLKF.js → p-DwemlMvt.js} +1 -1
  20. package/dist/components/{p-D8jD4OI9.js → p-zM6hQ55n.js} +1 -1
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/kritzel-active-users_42.entry.js +28 -3
  23. package/dist/esm/{workspace.migrations-BVtbsm5p.js → workspace.migrations-D5iSPf3E.js} +86 -58
  24. package/dist/stencil/index.esm.js +1 -1
  25. package/dist/stencil/p-D5iSPf3E.js +1 -0
  26. package/dist/stencil/p-c28b30ab.entry.js +9 -0
  27. package/dist/stencil/stencil.esm.js +1 -1
  28. package/dist/types/classes/objects/selection-group.class.d.ts +15 -1
  29. package/dist/types/classes/objects/shape.class.d.ts +0 -1
  30. package/dist/types/classes/objects/text.class.d.ts +0 -1
  31. package/dist/types/classes/structures/object-map.structure.d.ts +17 -0
  32. package/dist/types/constants/version.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/dist/stencil/p-56122555.entry.js +0 -9
  35. package/dist/stencil/p-BVtbsm5p.js +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var workspace_migrations = require('./workspace.migrations-BVBITEM5.js');
3
+ var workspace_migrations = require('./workspace.migrations-K5oVASsb.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-BVBITEM5.js');
4
+ var workspace_migrations = require('./workspace.migrations-K5oVASsb.js');
5
5
  var Y = require('yjs');
6
6
  require('y-websocket');
7
7
  require('y-indexeddb');
@@ -21354,6 +21354,12 @@ class KritzelObjectMap {
21354
21354
  _lastAwarenessEmitTime = 0;
21355
21355
  _awarenessEmitTimeout = null;
21356
21356
  AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
21357
+ /**
21358
+ * When true, update() only modifies local state (quadtree + idMap)
21359
+ * without writing to Yjs. Used during drag operations to avoid
21360
+ * redundant full serializations of every child object per frame.
21361
+ */
21362
+ _localOnlyMode = false;
21357
21363
  /**
21358
21364
  * Indicates whether the object map has been initialized and is ready for use.
21359
21365
  * @returns `true` if providers are connected and the map is operational
@@ -21907,6 +21913,25 @@ class KritzelObjectMap {
21907
21913
  this._ydoc.transact(callback, 'local');
21908
21914
  }
21909
21915
  }
21916
+ /**
21917
+ * Executes a callback where all update() calls only modify local state
21918
+ * (quadtree + idMap) without writing to Yjs. This avoids expensive full
21919
+ * serializations during continuous operations like dragging many objects.
21920
+ *
21921
+ * Objects updated inside this callback must be persisted later
21922
+ * (e.g., via a normal update() call at drag end).
21923
+ *
21924
+ * @param callback - The function containing operations to execute locally
21925
+ */
21926
+ withLocalUpdatesOnly(callback) {
21927
+ this._localOnlyMode = true;
21928
+ try {
21929
+ callback();
21930
+ }
21931
+ finally {
21932
+ this._localOnlyMode = false;
21933
+ }
21934
+ }
21910
21935
  /**
21911
21936
  * Loads all objects from the Yjs objects map into the local quadtree.
21912
21937
  * Clears the existing quadtree and repopulates it by deserializing each
@@ -21998,7 +22023,7 @@ class KritzelObjectMap {
21998
22023
  }
21999
22024
  this.quadtree.update(object);
22000
22025
  this._idMap.set(object.id, object);
22001
- if (this._objectsMap && this.isPersistable(object)) {
22026
+ if (!this._localOnlyMode && this._objectsMap && this.isPersistable(object)) {
22002
22027
  const serialized = object.serialize();
22003
22028
  const origin = options.temporary ? 'temporary' : 'local';
22004
22029
  this._ydoc?.transact(() => {
@@ -28708,7 +28733,7 @@ const KritzelPortal = class {
28708
28733
  * This file is auto-generated by the version bump scripts.
28709
28734
  * Do not modify manually.
28710
28735
  */
28711
- const KRITZEL_VERSION = '0.1.81';
28736
+ const KRITZEL_VERSION = '0.1.83';
28712
28737
 
28713
28738
  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)}`;
28714
28739
 
@@ -15250,12 +15250,6 @@ class KritzelText extends KritzelBaseObject {
15250
15250
  isEditing = false;
15251
15251
  editor = null;
15252
15252
  content = null;
15253
- get _editor() {
15254
- if (!this._editor) {
15255
- throw new Error('KritzelShape: editor is not initialized');
15256
- }
15257
- return this._editor;
15258
- }
15259
15253
  _schema = new Schema({
15260
15254
  nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block'),
15261
15255
  marks: schema.spec.marks,
@@ -15267,10 +15261,10 @@ class KritzelText extends KritzelBaseObject {
15267
15261
  * @returns `true` if the editor is empty or contains only whitespace, `false` otherwise.
15268
15262
  */
15269
15263
  get isEmpty() {
15270
- if (!this._editor) {
15264
+ if (!this.editor) {
15271
15265
  return true;
15272
15266
  }
15273
- const doc = this._editor.state.doc;
15267
+ const doc = this.editor.state.doc;
15274
15268
  if (doc.content.size === 0) {
15275
15269
  return true;
15276
15270
  }
@@ -15374,7 +15368,7 @@ class KritzelText extends KritzelBaseObject {
15374
15368
  element.style.fontFamily = this.fontFamily;
15375
15369
  element.style.fontSize = `${this.fontSize}pt`;
15376
15370
  element.style.color = KritzelColorHelper.resolveThemeColor(this.fontColor);
15377
- if (this.isMounted && this.elementRef === element && this._editor.dom.parentElement === element) {
15371
+ if (this.isMounted && this.elementRef === element && this.editor.dom.parentElement === element) {
15378
15372
  if (!this._core.store.state.isScaling && !this._core.store.state.isPanning) {
15379
15373
  requestAnimationFrame(() => this.adjustSizeOnInput());
15380
15374
  }
@@ -15384,7 +15378,7 @@ class KritzelText extends KritzelBaseObject {
15384
15378
  this.elementRef.style.whiteSpace = 'pre-wrap';
15385
15379
  this.elementRef.style.wordWrap = 'break-word';
15386
15380
  this.elementRef.innerHTML = '';
15387
- this.elementRef.appendChild(this._editor.dom);
15381
+ this.elementRef.appendChild(this.editor.dom);
15388
15382
  this.isMounted = true;
15389
15383
  requestAnimationFrame(() => this.adjustSizeOnInput());
15390
15384
  }
@@ -15402,8 +15396,8 @@ class KritzelText extends KritzelBaseObject {
15402
15396
  }),
15403
15397
  editable: () => false,
15404
15398
  dispatchTransaction: transaction => {
15405
- const newState = this._editor.state.apply(transaction);
15406
- this._editor.updateState(newState);
15399
+ const newState = this.editor.state.apply(transaction);
15400
+ this.editor.updateState(newState);
15407
15401
  if (transaction.docChanged) {
15408
15402
  this.content = newState.doc.toJSON();
15409
15403
  this.adjustSizeOnInput();
@@ -15421,11 +15415,11 @@ class KritzelText extends KritzelBaseObject {
15421
15415
  */
15422
15416
  setContent(content) {
15423
15417
  this.content = content;
15424
- if (this._editor && content) {
15425
- const newDoc = this._editor.state.schema.nodeFromJSON(content);
15426
- const tr = this._editor.state.tr.replaceWith(0, this._editor.state.doc.content.size, newDoc.content);
15418
+ if (this.editor && content) {
15419
+ const newDoc = this.editor.state.schema.nodeFromJSON(content);
15420
+ const tr = this.editor.state.tr.replaceWith(0, this.editor.state.doc.content.size, newDoc.content);
15427
15421
  tr.setMeta('fromRemote', true);
15428
- this._editor.dispatch(tr);
15422
+ this.editor.dispatch(tr);
15429
15423
  }
15430
15424
  }
15431
15425
  /**
@@ -15490,13 +15484,13 @@ class KritzelText extends KritzelBaseObject {
15490
15484
  * @param coords.y - Y coordinate for cursor placement.
15491
15485
  */
15492
15486
  focus(coords) {
15493
- if (this._editor) {
15494
- const doc = this._editor.state.doc;
15487
+ if (this.editor) {
15488
+ const doc = this.editor.state.doc;
15495
15489
  if (coords.x && coords.y && !this.isEmpty) {
15496
- const pos = this._editor.posAtCoords({ left: coords.x, top: coords.y });
15490
+ const pos = this.editor.posAtCoords({ left: coords.x, top: coords.y });
15497
15491
  if (pos) {
15498
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
15499
- this._editor.focus();
15492
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
15493
+ this.editor.focus();
15500
15494
  if (KritzelDevicesHelper.isIOS()) {
15501
15495
  this.scrollIntoViewOnIOS();
15502
15496
  }
@@ -15504,8 +15498,8 @@ class KritzelText extends KritzelBaseObject {
15504
15498
  }
15505
15499
  }
15506
15500
  const end = Math.max(1, doc.content.size - 1);
15507
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, end)));
15508
- this._editor.focus();
15501
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, end)));
15502
+ this.editor.focus();
15509
15503
  if (KritzelDevicesHelper.isIOS()) {
15510
15504
  this.scrollIntoViewOnIOS();
15511
15505
  }
@@ -15517,8 +15511,8 @@ class KritzelText extends KritzelBaseObject {
15517
15511
  */
15518
15512
  scrollIntoViewOnIOS() {
15519
15513
  setTimeout(() => {
15520
- if (this._editor && this._editor.dom) {
15521
- this._editor.dom.scrollIntoView({
15514
+ if (this.editor && this.editor.dom) {
15515
+ this.editor.dom.scrollIntoView({
15522
15516
  behavior: 'smooth',
15523
15517
  block: 'center',
15524
15518
  inline: 'nearest',
@@ -15536,7 +15530,7 @@ class KritzelText extends KritzelBaseObject {
15536
15530
  KritzelKeyboardHelper.disableInteractiveWidget();
15537
15531
  this.uneditedObject = this.clone();
15538
15532
  this._core.store.setState('activeTool', KritzelToolRegistry.getTool('text'));
15539
- this._editor.setProps({ editable: () => true });
15533
+ this.editor.setProps({ editable: () => true });
15540
15534
  this.isEditing = true;
15541
15535
  this._core.rerender();
15542
15536
  this.adjustSizeOnInput();
@@ -15552,9 +15546,9 @@ class KritzelText extends KritzelBaseObject {
15552
15546
  */
15553
15547
  save() {
15554
15548
  requestAnimationFrame(() => this.adjustSizeOnInput());
15555
- this.content = this._editor.state.doc.toJSON();
15556
- this._editor.setProps({ editable: () => false });
15557
- this._editor.dom.blur();
15549
+ this.content = this.editor.state.doc.toJSON();
15550
+ this.editor.setProps({ editable: () => false });
15551
+ this.editor.dom.blur();
15558
15552
  this.isEditing = false;
15559
15553
  this._core.store.objects.consolidateTemporaryItems();
15560
15554
  this._core.store.objects.update(this);
@@ -17645,12 +17639,6 @@ class KritzelShape extends KritzelBaseObject {
17645
17639
  get viewBox() {
17646
17640
  return `${this.x} ${this.y} ${this.width} ${this.height}`;
17647
17641
  }
17648
- get _editor() {
17649
- if (!this.editor) {
17650
- throw new Error('KritzelShape: editor is not initialized');
17651
- }
17652
- return this.editor;
17653
- }
17654
17642
  /**
17655
17643
  * Creates a new KritzelShape instance with optional configuration.
17656
17644
  * This constructor initializes the shape with default values that can be
@@ -17758,7 +17746,7 @@ class KritzelShape extends KritzelBaseObject {
17758
17746
  if (element === null || this.isInViewport() === false) {
17759
17747
  return;
17760
17748
  }
17761
- if (this.isMounted && this.elementRef === element && this._editor.dom.parentElement === element) {
17749
+ if (this.isMounted && this.elementRef === element && this.editor.dom.parentElement === element) {
17762
17750
  return;
17763
17751
  }
17764
17752
  this.elementRef = element;
@@ -17774,7 +17762,7 @@ class KritzelShape extends KritzelBaseObject {
17774
17762
  * @returns void
17775
17763
  */
17776
17764
  mountTextEditor(element) {
17777
- if (element === null || !this.editor) {
17765
+ if (element === null) {
17778
17766
  return;
17779
17767
  }
17780
17768
  element.style.fontFamily = this.fontFamily;
@@ -17805,8 +17793,8 @@ class KritzelShape extends KritzelBaseObject {
17805
17793
  }),
17806
17794
  editable: () => false,
17807
17795
  dispatchTransaction: transaction => {
17808
- const newState = this._editor.state.apply(transaction);
17809
- this._editor.updateState(newState);
17796
+ const newState = this.editor.state.apply(transaction);
17797
+ this.editor.updateState(newState);
17810
17798
  if (transaction.docChanged) {
17811
17799
  this.content = newState.doc.toJSON();
17812
17800
  if (!transaction.getMeta('fromRemote')) {
@@ -17868,12 +17856,12 @@ class KritzelShape extends KritzelBaseObject {
17868
17856
  * @returns void
17869
17857
  */
17870
17858
  focus(coords) {
17871
- const doc = this._editor.state.doc;
17859
+ const doc = this.editor.state.doc;
17872
17860
  if (coords?.x && coords?.y) {
17873
- const pos = this._editor.posAtCoords({ left: coords.x, top: coords.y });
17861
+ const pos = this.editor.posAtCoords({ left: coords.x, top: coords.y });
17874
17862
  if (pos) {
17875
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
17876
- this._editor.focus();
17863
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
17864
+ this.editor.focus();
17877
17865
  if (KritzelDevicesHelper.isIOS()) {
17878
17866
  this.scrollIntoViewOnIOS();
17879
17867
  }
@@ -17881,8 +17869,8 @@ class KritzelShape extends KritzelBaseObject {
17881
17869
  }
17882
17870
  }
17883
17871
  const end = Math.max(1, doc.content.size - 1);
17884
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, end)));
17885
- this._editor.focus();
17872
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, end)));
17873
+ this.editor.focus();
17886
17874
  if (KritzelDevicesHelper.isIOS()) {
17887
17875
  this.scrollIntoViewOnIOS();
17888
17876
  }
@@ -17920,7 +17908,7 @@ class KritzelShape extends KritzelBaseObject {
17920
17908
  KritzelKeyboardHelper.disableInteractiveWidget();
17921
17909
  this.uneditedObject = this.clone();
17922
17910
  this._core.store.setState('activeTool', KritzelToolRegistry.getTool('shape'));
17923
- this._editor.setProps({ editable: () => true });
17911
+ this.editor.setProps({ editable: () => true });
17924
17912
  this.isEditing = true;
17925
17913
  this._core.rerender();
17926
17914
  if (event?.clientX && event?.clientY) {
@@ -17937,9 +17925,9 @@ class KritzelShape extends KritzelBaseObject {
17937
17925
  * @returns void
17938
17926
  */
17939
17927
  save() {
17940
- this.content = this._editor.state.doc.toJSON();
17941
- this._editor.setProps({ editable: () => false });
17942
- this._editor.dom.blur();
17928
+ this.content = this.editor.state.doc.toJSON();
17929
+ this.editor.setProps({ editable: () => false });
17930
+ this.editor.dom.blur();
17943
17931
  this.isEditing = false;
17944
17932
  this._core.store.objects.consolidateTemporaryItems();
17945
17933
  this._core.store.objects.update(this);
@@ -18555,6 +18543,10 @@ class KritzelSelectionGroup extends KritzelBaseObject {
18555
18543
  handleColor;
18556
18544
  handleStrokeColor;
18557
18545
  handleSize = 6;
18546
+ /** Timestamp of the last Yjs persist for children during a drag */
18547
+ _lastChildPersistTime = 0;
18548
+ /** Minimum interval (ms) between child Yjs persists during drag */
18549
+ static CHILD_PERSIST_THROTTLE_MS = 100;
18558
18550
  /**
18559
18551
  * Gets the array of object IDs contained in this selection group.
18560
18552
  * @returns Array of string IDs for the selected objects
@@ -18840,7 +18832,11 @@ class KritzelSelectionGroup extends KritzelBaseObject {
18840
18832
  /**
18841
18833
  * Moves the selection group and all its contained objects by calculating the delta from drag coordinates.
18842
18834
  * Updates anchor points for any lines connected to the moved objects.
18843
- * Uses a transaction to batch all object updates for performance.
18835
+ *
18836
+ * Child objects are moved locally without writing to Yjs during the drag to avoid
18837
+ * N+1 full serializations per frame, which can overwhelm the WebSocket server.
18838
+ * Children must be persisted at drag end via {@link persistChildren}.
18839
+ *
18844
18840
  * @param startX - The starting x-coordinate of the drag operation (in screen space)
18845
18841
  * @param startY - The starting y-coordinate of the drag operation (in screen space)
18846
18842
  * @param endX - The ending x-coordinate of the drag operation (in screen space)
@@ -18851,21 +18847,51 @@ class KritzelSelectionGroup extends KritzelBaseObject {
18851
18847
  const deltaY = (startY - endY) / this._core.store.state.scale;
18852
18848
  this.translateX += deltaX;
18853
18849
  this.translateY += deltaY;
18854
- this._core.store.objects.transaction(() => {
18850
+ const now = Date.now();
18851
+ const shouldPersistChildren = now - this._lastChildPersistTime >= KritzelSelectionGroup.CHILD_PERSIST_THROTTLE_MS;
18852
+ if (shouldPersistChildren) {
18853
+ // Periodically persist children to Yjs so remote peers see movement in real-time
18854
+ this._core.store.objects.transaction(() => {
18855
+ this._core.store.objects.update(this);
18856
+ const children = this.objects;
18857
+ for (const obj of children) {
18858
+ obj.move(startX, startY, endX, endY);
18859
+ this._core.anchorManager.updateAnchorsForObject(obj.id);
18860
+ }
18861
+ });
18862
+ this._lastChildPersistTime = now;
18863
+ }
18864
+ else {
18865
+ // Between throttle intervals, only persist the selection group itself.
18866
+ // Children are moved locally for smooth local rendering.
18855
18867
  this._core.store.objects.update(this);
18856
- const children = this.objects;
18857
- for (const obj of children) {
18858
- obj.move(startX, startY, endX, endY);
18859
- // Update any lines that are anchored to this object
18860
- this._core.anchorManager.updateAnchorsForObject(obj.id);
18861
- }
18862
- });
18868
+ this._core.store.objects.withLocalUpdatesOnly(() => {
18869
+ const children = this.objects;
18870
+ for (const obj of children) {
18871
+ obj.move(startX, startY, endX, endY);
18872
+ this._core.anchorManager.updateAnchorsForObject(obj.id);
18873
+ }
18874
+ });
18875
+ }
18863
18876
  // Update snapshots
18864
18877
  this.unchangedObjectSnapshots.forEach(snapshot => {
18865
18878
  snapshot.translateX += deltaX;
18866
18879
  snapshot.translateY += deltaY;
18867
18880
  });
18868
18881
  }
18882
+ /**
18883
+ * Persists all child objects to Yjs in a single transaction.
18884
+ * Must be called after a drag operation completes to sync the final
18885
+ * positions of children that were only updated locally during the drag.
18886
+ */
18887
+ persistChildren() {
18888
+ this._core.store.objects.transaction(() => {
18889
+ const children = this.objects;
18890
+ for (const obj of children) {
18891
+ this._core.store.objects.update(obj);
18892
+ }
18893
+ });
18894
+ }
18869
18895
  /**
18870
18896
  * Resizes the selection group and scales all contained objects proportionally.
18871
18897
  * Uses snapshot values to avoid compounding errors during continuous drag operations.
@@ -20379,6 +20405,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
20379
20405
  if (this._core.store.state.isDragging) {
20380
20406
  this._core.store.state.isDragging = false;
20381
20407
  if (this.hasMoved) {
20408
+ this._core.store.selectionGroup.persistChildren();
20382
20409
  this._core.store.selectionGroup.update();
20383
20410
  this._core.engine.emitObjectsChange();
20384
20411
  this._core.store.state.hasObjectsChanged = true;
@@ -20389,6 +20416,7 @@ class KritzelMoveHandler extends KritzelBaseHandler {
20389
20416
  if (this._core.store.state.isDragging) {
20390
20417
  this._core.store.state.isDragging = false;
20391
20418
  if (this.hasMoved) {
20419
+ this._core.store.selectionGroup.persistChildren();
20392
20420
  this._core.store.selectionGroup.update();
20393
20421
  this._core.engine.emitObjectsChange();
20394
20422
  this._core.store.state.hasObjectsChanged = true;
@@ -210,6 +210,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
210
210
  if (this._core.store.state.isDragging) {
211
211
  this._core.store.state.isDragging = false;
212
212
  if (this.hasMoved) {
213
+ this._core.store.selectionGroup.persistChildren();
213
214
  this._core.store.selectionGroup.update();
214
215
  this._core.engine.emitObjectsChange();
215
216
  this._core.store.state.hasObjectsChanged = true;
@@ -220,6 +221,7 @@ export class KritzelMoveHandler extends KritzelBaseHandler {
220
221
  if (this._core.store.state.isDragging) {
221
222
  this._core.store.state.isDragging = false;
222
223
  if (this.hasMoved) {
224
+ this._core.store.selectionGroup.persistChildren();
223
225
  this._core.store.selectionGroup.update();
224
226
  this._core.engine.emitObjectsChange();
225
227
  this._core.store.state.hasObjectsChanged = true;
@@ -29,6 +29,10 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
29
29
  handleColor;
30
30
  handleStrokeColor;
31
31
  handleSize = 6;
32
+ /** Timestamp of the last Yjs persist for children during a drag */
33
+ _lastChildPersistTime = 0;
34
+ /** Minimum interval (ms) between child Yjs persists during drag */
35
+ static CHILD_PERSIST_THROTTLE_MS = 100;
32
36
  /**
33
37
  * Gets the array of object IDs contained in this selection group.
34
38
  * @returns Array of string IDs for the selected objects
@@ -314,7 +318,11 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
314
318
  /**
315
319
  * Moves the selection group and all its contained objects by calculating the delta from drag coordinates.
316
320
  * Updates anchor points for any lines connected to the moved objects.
317
- * Uses a transaction to batch all object updates for performance.
321
+ *
322
+ * Child objects are moved locally without writing to Yjs during the drag to avoid
323
+ * N+1 full serializations per frame, which can overwhelm the WebSocket server.
324
+ * Children must be persisted at drag end via {@link persistChildren}.
325
+ *
318
326
  * @param startX - The starting x-coordinate of the drag operation (in screen space)
319
327
  * @param startY - The starting y-coordinate of the drag operation (in screen space)
320
328
  * @param endX - The ending x-coordinate of the drag operation (in screen space)
@@ -325,21 +333,51 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
325
333
  const deltaY = (startY - endY) / this._core.store.state.scale;
326
334
  this.translateX += deltaX;
327
335
  this.translateY += deltaY;
328
- this._core.store.objects.transaction(() => {
336
+ const now = Date.now();
337
+ const shouldPersistChildren = now - this._lastChildPersistTime >= KritzelSelectionGroup.CHILD_PERSIST_THROTTLE_MS;
338
+ if (shouldPersistChildren) {
339
+ // Periodically persist children to Yjs so remote peers see movement in real-time
340
+ this._core.store.objects.transaction(() => {
341
+ this._core.store.objects.update(this);
342
+ const children = this.objects;
343
+ for (const obj of children) {
344
+ obj.move(startX, startY, endX, endY);
345
+ this._core.anchorManager.updateAnchorsForObject(obj.id);
346
+ }
347
+ });
348
+ this._lastChildPersistTime = now;
349
+ }
350
+ else {
351
+ // Between throttle intervals, only persist the selection group itself.
352
+ // Children are moved locally for smooth local rendering.
329
353
  this._core.store.objects.update(this);
330
- const children = this.objects;
331
- for (const obj of children) {
332
- obj.move(startX, startY, endX, endY);
333
- // Update any lines that are anchored to this object
334
- this._core.anchorManager.updateAnchorsForObject(obj.id);
335
- }
336
- });
354
+ this._core.store.objects.withLocalUpdatesOnly(() => {
355
+ const children = this.objects;
356
+ for (const obj of children) {
357
+ obj.move(startX, startY, endX, endY);
358
+ this._core.anchorManager.updateAnchorsForObject(obj.id);
359
+ }
360
+ });
361
+ }
337
362
  // Update snapshots
338
363
  this.unchangedObjectSnapshots.forEach(snapshot => {
339
364
  snapshot.translateX += deltaX;
340
365
  snapshot.translateY += deltaY;
341
366
  });
342
367
  }
368
+ /**
369
+ * Persists all child objects to Yjs in a single transaction.
370
+ * Must be called after a drag operation completes to sync the final
371
+ * positions of children that were only updated locally during the drag.
372
+ */
373
+ persistChildren() {
374
+ this._core.store.objects.transaction(() => {
375
+ const children = this.objects;
376
+ for (const obj of children) {
377
+ this._core.store.objects.update(obj);
378
+ }
379
+ });
380
+ }
343
381
  /**
344
382
  * Resizes the selection group and scales all contained objects proportionally.
345
383
  * Uses snapshot values to avoid compounding errors during continuous drag operations.
@@ -44,12 +44,6 @@ export class KritzelShape extends KritzelBaseObject {
44
44
  get viewBox() {
45
45
  return `${this.x} ${this.y} ${this.width} ${this.height}`;
46
46
  }
47
- get _editor() {
48
- if (!this.editor) {
49
- throw new Error('KritzelShape: editor is not initialized');
50
- }
51
- return this.editor;
52
- }
53
47
  /**
54
48
  * Creates a new KritzelShape instance with optional configuration.
55
49
  * This constructor initializes the shape with default values that can be
@@ -157,7 +151,7 @@ export class KritzelShape extends KritzelBaseObject {
157
151
  if (element === null || this.isInViewport() === false) {
158
152
  return;
159
153
  }
160
- if (this.isMounted && this.elementRef === element && this._editor.dom.parentElement === element) {
154
+ if (this.isMounted && this.elementRef === element && this.editor.dom.parentElement === element) {
161
155
  return;
162
156
  }
163
157
  this.elementRef = element;
@@ -173,7 +167,7 @@ export class KritzelShape extends KritzelBaseObject {
173
167
  * @returns void
174
168
  */
175
169
  mountTextEditor(element) {
176
- if (element === null || !this.editor) {
170
+ if (element === null) {
177
171
  return;
178
172
  }
179
173
  element.style.fontFamily = this.fontFamily;
@@ -204,8 +198,8 @@ export class KritzelShape extends KritzelBaseObject {
204
198
  }),
205
199
  editable: () => false,
206
200
  dispatchTransaction: transaction => {
207
- const newState = this._editor.state.apply(transaction);
208
- this._editor.updateState(newState);
201
+ const newState = this.editor.state.apply(transaction);
202
+ this.editor.updateState(newState);
209
203
  if (transaction.docChanged) {
210
204
  this.content = newState.doc.toJSON();
211
205
  if (!transaction.getMeta('fromRemote')) {
@@ -267,12 +261,12 @@ export class KritzelShape extends KritzelBaseObject {
267
261
  * @returns void
268
262
  */
269
263
  focus(coords) {
270
- const doc = this._editor.state.doc;
264
+ const doc = this.editor.state.doc;
271
265
  if (coords?.x && coords?.y) {
272
- const pos = this._editor.posAtCoords({ left: coords.x, top: coords.y });
266
+ const pos = this.editor.posAtCoords({ left: coords.x, top: coords.y });
273
267
  if (pos) {
274
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
275
- this._editor.focus();
268
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, pos.pos)));
269
+ this.editor.focus();
276
270
  if (KritzelDevicesHelper.isIOS()) {
277
271
  this.scrollIntoViewOnIOS();
278
272
  }
@@ -280,8 +274,8 @@ export class KritzelShape extends KritzelBaseObject {
280
274
  }
281
275
  }
282
276
  const end = Math.max(1, doc.content.size - 1);
283
- this._editor.dispatch(this._editor.state.tr.setSelection(TextSelection.create(doc, end)));
284
- this._editor.focus();
277
+ this.editor.dispatch(this.editor.state.tr.setSelection(TextSelection.create(doc, end)));
278
+ this.editor.focus();
285
279
  if (KritzelDevicesHelper.isIOS()) {
286
280
  this.scrollIntoViewOnIOS();
287
281
  }
@@ -319,7 +313,7 @@ export class KritzelShape extends KritzelBaseObject {
319
313
  KritzelKeyboardHelper.disableInteractiveWidget();
320
314
  this.uneditedObject = this.clone();
321
315
  this._core.store.setState('activeTool', KritzelToolRegistry.getTool('shape'));
322
- this._editor.setProps({ editable: () => true });
316
+ this.editor.setProps({ editable: () => true });
323
317
  this.isEditing = true;
324
318
  this._core.rerender();
325
319
  if (event?.clientX && event?.clientY) {
@@ -336,9 +330,9 @@ export class KritzelShape extends KritzelBaseObject {
336
330
  * @returns void
337
331
  */
338
332
  save() {
339
- this.content = this._editor.state.doc.toJSON();
340
- this._editor.setProps({ editable: () => false });
341
- this._editor.dom.blur();
333
+ this.content = this.editor.state.doc.toJSON();
334
+ this.editor.setProps({ editable: () => false });
335
+ this.editor.dom.blur();
342
336
  this.isEditing = false;
343
337
  this._core.store.objects.consolidateTemporaryItems();
344
338
  this._core.store.objects.update(this);