kritzel-stencil 0.2.0 → 0.2.2

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 (76) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +177 -52
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/stencil.cjs.js +1 -1
  5. package/dist/cjs/{workspace.migrations-CPncKohl.js → workspace.migrations-BPwtowiJ.js} +208 -24
  6. package/dist/collection/classes/core/core.class.js +27 -19
  7. package/dist/collection/classes/core/viewport.class.js +3 -0
  8. package/dist/collection/classes/handlers/key.handler.js +73 -6
  9. package/dist/collection/classes/handlers/selection.handler.js +36 -2
  10. package/dist/collection/classes/objects/group.class.js +69 -12
  11. package/dist/collection/classes/objects/shape.class.js +24 -0
  12. package/dist/collection/classes/objects/text.class.js +23 -0
  13. package/dist/collection/classes/tools/text-tool.class.js +46 -10
  14. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +28 -7
  15. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +64 -18
  16. package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +23 -1
  17. package/dist/collection/constants/version.js +1 -1
  18. package/dist/collection/themes/dark-theme.js +5 -0
  19. package/dist/collection/themes/light-theme.js +5 -0
  20. package/dist/components/index.js +1 -1
  21. package/dist/components/kritzel-awareness-cursors.js +1 -1
  22. package/dist/components/kritzel-color-palette.js +1 -1
  23. package/dist/components/kritzel-color.js +1 -1
  24. package/dist/components/kritzel-controls.js +1 -1
  25. package/dist/components/kritzel-editor.js +1 -1
  26. package/dist/components/kritzel-engine.js +1 -1
  27. package/dist/components/kritzel-menu-item.js +1 -1
  28. package/dist/components/kritzel-menu.js +1 -1
  29. package/dist/components/kritzel-more-menu.js +1 -1
  30. package/dist/components/kritzel-portal.js +1 -1
  31. package/dist/components/kritzel-settings.js +1 -1
  32. package/dist/components/kritzel-split-button.js +1 -1
  33. package/dist/components/kritzel-stroke-size.js +1 -1
  34. package/dist/components/kritzel-tool-config.js +1 -1
  35. package/dist/components/kritzel-workspace-manager.js +1 -1
  36. package/dist/components/{p-CFzvz-B2.js → p-0YBCp8Wh.js} +1 -1
  37. package/dist/components/{p-DkT0CXfN.js → p-574MVXxi.js} +1 -1
  38. package/dist/components/p-BCzbwL4m.js +1 -0
  39. package/dist/components/p-BLjdzUzs.js +1 -0
  40. package/dist/components/{p-BFQVg_eQ.js → p-BSEdLfq2.js} +1 -1
  41. package/dist/components/{p-C3Dwuqka.js → p-BWrxz4mM.js} +1 -1
  42. package/dist/components/{p-ChqeIKg_.js → p-BYOIzv_f.js} +1 -1
  43. package/dist/components/{p-CekG3_ce.js → p-Bfa-Amjn.js} +1 -1
  44. package/dist/components/{p-9ASFIqd0.js → p-BmcAX-1k.js} +1 -1
  45. package/dist/components/{p-CzIuqMQA.js → p-BtJB7FsW.js} +1 -1
  46. package/dist/components/{p-C_yfHS4F.js → p-C6Td7I4k.js} +1 -1
  47. package/dist/components/{p-ChQNi67Z.js → p-D9ifYAtg.js} +1 -1
  48. package/dist/components/{p-B_rHzy0t.js → p-DE2xDwUM.js} +1 -1
  49. package/dist/components/{p-CaDBSaxZ.js → p-DFeyobdy.js} +2 -2
  50. package/dist/components/{p-BHSRRiEg.js → p-DfB7uJ0N.js} +1 -1
  51. package/dist/components/{p-B2kHVHa_.js → p-u-827ZX7.js} +1 -1
  52. package/dist/esm/index.js +2 -2
  53. package/dist/esm/kritzel-active-users_42.entry.js +177 -52
  54. package/dist/esm/loader.js +1 -1
  55. package/dist/esm/stencil.js +1 -1
  56. package/dist/esm/{workspace.migrations-ytjzXm9B.js → workspace.migrations-C_uxbvuH.js} +208 -24
  57. package/dist/stencil/index.esm.js +1 -1
  58. package/dist/stencil/p-4d28c496.entry.js +9 -0
  59. package/dist/stencil/p-C_uxbvuH.js +1 -0
  60. package/dist/stencil/stencil.esm.js +1 -1
  61. package/dist/types/classes/handlers/selection.handler.d.ts +15 -0
  62. package/dist/types/classes/objects/group.class.d.ts +15 -0
  63. package/dist/types/classes/objects/shape.class.d.ts +10 -0
  64. package/dist/types/classes/objects/text.class.d.ts +9 -0
  65. package/dist/types/classes/tools/text-tool.class.d.ts +26 -8
  66. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +1 -0
  67. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +2 -0
  68. package/dist/types/components/shared/kritzel-portal/kritzel-portal.d.ts +1 -0
  69. package/dist/types/components.d.ts +7 -2
  70. package/dist/types/constants/version.d.ts +1 -1
  71. package/dist/types/interfaces/theme.interface.d.ts +12 -4
  72. package/package.json +1 -1
  73. package/dist/components/p-BYX50YSd.js +0 -1
  74. package/dist/components/p-CjazGGq3.js +0 -1
  75. package/dist/stencil/p-19e04f32.entry.js +0 -9
  76. package/dist/stencil/p-ytjzXm9B.js +0 -1
@@ -111,6 +111,23 @@ export class KritzelGroup extends KritzelBaseObject {
111
111
  }
112
112
  return null;
113
113
  }
114
+ /**
115
+ * Recursively collects the IDs of all descendants of the given group (children,
116
+ * grandchildren, etc.). Useful for detecting when a selection contains both an
117
+ * ancestor group and one of its descendants — a configuration that would otherwise
118
+ * cause the descendant to be transformed twice (once via the ancestor, once directly).
119
+ * @param group - The group whose descendants should be collected.
120
+ * @param out - A set that will be populated with all descendant object IDs.
121
+ */
122
+ static collectDescendantIds(group, out = new Set()) {
123
+ for (const child of group.children) {
124
+ out.add(child.id);
125
+ if (child instanceof KritzelGroup) {
126
+ KritzelGroup.collectDescendantIds(child, out);
127
+ }
128
+ }
129
+ return out;
130
+ }
114
131
  /**
115
132
  * Adds a child object to this group.
116
133
  * If the object is already a child, no action is taken.
@@ -154,8 +171,19 @@ export class KritzelGroup extends KritzelBaseObject {
154
171
  * Finalizes the group after children have been positioned (e.g., after paste).
155
172
  * Refreshes the bounding box to encompass all children and captures
156
173
  * child snapshots for subsequent transformation operations.
174
+ *
175
+ * Recursively finalizes nested child groups first, so that when a transform
176
+ * (rotate/resize) cascades through nested groups, every group's snapshots are
177
+ * aligned with the current visual state. Without this, a nested group's
178
+ * `snapshotRotation` could be stale, causing the inner group to be offset
179
+ * relative to its parent during rotation.
157
180
  */
158
181
  finalize() {
182
+ for (const child of this.children) {
183
+ if (child instanceof KritzelGroup) {
184
+ child.finalize();
185
+ }
186
+ }
159
187
  this.refreshBoundingBox();
160
188
  this.captureChildSnapshots();
161
189
  }
@@ -312,11 +340,23 @@ export class KritzelGroup extends KritzelBaseObject {
312
340
  * @param height - The new height of the group's content area.
313
341
  */
314
342
  resize(x, y, width, height) {
315
- const widthScaleFactor = width / this.width;
316
- const heightScaleFactor = height / this.height;
317
- // Calculate old center
318
- const oldCenterX = this.translateX + this.totalWidth / 2 / this.scale;
319
- const oldCenterY = this.translateY + this.totalHeight / 2 / this.scale;
343
+ // Use snapshot dimensions (stable across the gesture) instead of `this.*`,
344
+ // which mutates each frame and gets distorted by descendants like KritzelText
345
+ // that clamp to a uniform scale, causing the cascade factor to drift.
346
+ const baseWidth = this.snapshotTotalWidth > 0
347
+ ? this.snapshotTotalWidth - this.padding * 2
348
+ : this.width;
349
+ const baseHeight = this.snapshotTotalHeight > 0
350
+ ? this.snapshotTotalHeight - this.padding * 2
351
+ : this.height;
352
+ const baseScale = this.snapshotScale || this.scale || 1;
353
+ const widthScaleFactor = baseWidth !== 0 ? width / baseWidth : 1;
354
+ const heightScaleFactor = baseHeight !== 0 ? height / baseHeight : 1;
355
+ // Calculate old center from snapshot (stable across the gesture)
356
+ const snapshotTotalWidth = this.snapshotTotalWidth || (this.width + this.padding * 2);
357
+ const snapshotTotalHeight = this.snapshotTotalHeight || (this.height + this.padding * 2);
358
+ const oldCenterX = this.snapshotTranslateX + snapshotTotalWidth / 2 / baseScale;
359
+ const oldCenterY = this.snapshotTranslateY + snapshotTotalHeight / 2 / baseScale;
320
360
  // Calculate new center
321
361
  const newTotalWidth = width + this.padding * 2;
322
362
  const newTotalHeight = height + this.padding * 2;
@@ -329,9 +369,20 @@ export class KritzelGroup extends KritzelBaseObject {
329
369
  const sinR = Math.sin(rotation);
330
370
  this._core.store.objects.transaction(() => {
331
371
  this.children.forEach(child => {
332
- // Calculate child center
333
- const childCenterX = child.translateX + child.totalWidth / 2 / child.scale;
334
- const childCenterY = child.translateY + child.totalHeight / 2 / child.scale;
372
+ // Use snapshot values for the child to avoid compounding distortions caused
373
+ // by descendants like text that cannot honour requested aspect ratios.
374
+ const snapshot = this.unchangedChildSnapshots.get(child.id);
375
+ const childTranslateX = snapshot ? snapshot.translateX : child.translateX;
376
+ const childTranslateY = snapshot ? snapshot.translateY : child.translateY;
377
+ const childTotalWidth = snapshot ? snapshot.totalWidth : child.totalWidth;
378
+ const childTotalHeight = snapshot ? snapshot.totalHeight : child.totalHeight;
379
+ const childWidth = snapshot ? snapshot.width : child.width;
380
+ const childHeight = snapshot ? snapshot.height : child.height;
381
+ const childRotation = snapshot ? snapshot.rotation : child.rotation;
382
+ const childScale = (snapshot ? snapshot.scale : child.scale) || 1;
383
+ // Calculate child center from snapshot
384
+ const childCenterX = childTranslateX + childTotalWidth / 2 / childScale;
385
+ const childCenterY = childTranslateY + childTotalHeight / 2 / childScale;
335
386
  // Vector from old group center to child center
336
387
  const dx = childCenterX - oldCenterX;
337
388
  const dy = childCenterY - oldCenterY;
@@ -348,13 +399,13 @@ export class KritzelGroup extends KritzelBaseObject {
348
399
  const newChildCenterX = newCenterX + rotatedX;
349
400
  const newChildCenterY = newCenterY + rotatedY;
350
401
  // Calculate relative rotation for scaling
351
- const relativeRotation = child.rotation - rotation;
402
+ const relativeRotation = childRotation - rotation;
352
403
  const cosRel = Math.cos(relativeRotation);
353
404
  const sinRel = Math.sin(relativeRotation);
354
405
  const newChildWidthScale = Math.sqrt(Math.pow(widthScaleFactor * cosRel, 2) + Math.pow(heightScaleFactor * sinRel, 2));
355
406
  const newChildHeightScale = Math.sqrt(Math.pow(widthScaleFactor * sinRel, 2) + Math.pow(heightScaleFactor * cosRel, 2));
356
- const updatedWidth = child.width * newChildWidthScale;
357
- const updatedHeight = child.height * newChildHeightScale;
407
+ const updatedWidth = childWidth * newChildWidthScale;
408
+ const updatedHeight = childHeight * newChildHeightScale;
358
409
  const updatedTotalWidth = updatedWidth + child.padding * 2;
359
410
  const updatedTotalHeight = updatedHeight + child.padding * 2;
360
411
  const updatedX = newChildCenterX - updatedTotalWidth / 2 / child.scale;
@@ -364,7 +415,13 @@ export class KritzelGroup extends KritzelBaseObject {
364
415
  this._core.anchorManager.updateAnchorsForObject(child.id);
365
416
  });
366
417
  this.refreshBoundingBox();
367
- this.captureChildSnapshots();
418
+ // Don't recapture snapshots while an interactive resize is in progress —
419
+ // recapturing each frame would defeat the purpose of using stable snapshots
420
+ // and re-introduce the compounding distortion described above. Snapshots are
421
+ // refreshed at the start of the next gesture via beginTransform/finalize.
422
+ if (!this._core.store.state.isResizing) {
423
+ this.captureChildSnapshots();
424
+ }
368
425
  this._core.store.objects.update(this);
369
426
  });
370
427
  }
@@ -341,6 +341,30 @@ export class KritzelShape extends KritzelBaseObject {
341
341
  this._core.store.objects.update(this);
342
342
  this._core.engine.emitObjectsChange();
343
343
  }
344
+ /**
345
+ * Handles the Escape key while the shape's text editor is in edit mode.
346
+ * Implements a two-stage behavior: if the editor currently has a non-empty
347
+ * text selection, the selection is collapsed to a caret without exiting
348
+ * edit mode. If the selection is already collapsed, edit mode is exited
349
+ * (saving the current content), the canvas selection is cleared and the
350
+ * active tool is switched back to the selection tool so subsequent
351
+ * interactions start fresh.
352
+ */
353
+ handleEscape() {
354
+ if (!this.editor || !this.isEditing) {
355
+ return;
356
+ }
357
+ const { state } = this.editor;
358
+ if (!state.selection.empty) {
359
+ const pos = state.selection.head;
360
+ this.editor.dispatch(state.tr.setSelection(TextSelection.create(state.doc, pos)));
361
+ this.editor.focus();
362
+ return;
363
+ }
364
+ this._core.resetActiveShape();
365
+ this._core.clearSelection();
366
+ this._core.store.setState('activeTool', KritzelToolRegistry.getTool('selection'));
367
+ }
344
368
  /**
345
369
  * Handles pointer down events when the shape is in edit mode.
346
370
  * This method stops event propagation for untracked pointers to prevent
@@ -332,6 +332,29 @@ export class KritzelText extends KritzelBaseObject {
332
332
  this._core.store.objects.update(this);
333
333
  this._core.engine.emitObjectsChange();
334
334
  }
335
+ /**
336
+ * Handles the Escape key while the text object is in edit mode.
337
+ * Implements a two-stage behavior: if the editor currently has a non-empty
338
+ * text selection, the selection is collapsed to a caret without exiting
339
+ * edit mode. If the selection is already collapsed, edit mode is exited
340
+ * (saving the current content or deleting the object if it is empty) and
341
+ * the canvas selection is cleared so subsequent tool actions start fresh.
342
+ */
343
+ handleEscape() {
344
+ if (!this.editor || !this.isEditing) {
345
+ return;
346
+ }
347
+ const { state } = this.editor;
348
+ if (!state.selection.empty) {
349
+ const pos = state.selection.head;
350
+ this.editor.dispatch(state.tr.setSelection(TextSelection.create(state.doc, pos)));
351
+ this.editor.focus();
352
+ return;
353
+ }
354
+ this._core.resetActiveText();
355
+ this._core.clearSelection();
356
+ this._core.store.setState('activeTool', KritzelToolRegistry.getTool('selection'));
357
+ }
335
358
  /**
336
359
  * Handles pointer down events during text editing.
337
360
  * Stops event propagation to prevent canvas interaction while editing.
@@ -10,14 +10,38 @@ import { DEFAULT_COLOR_PALETTE } from "../../constants/color-palette.constants";
10
10
  * Supports configurable font family, size, color, and opacity.
11
11
  */
12
12
  export class KritzelTextTool extends KritzelBaseTool {
13
- /** The font family for new text objects */
14
- fontFamily = 'Arial';
15
- /** The font size for new text objects in pixels */
16
- fontSize = 16;
17
- /** The font color for new text objects (supports theme-aware light/dark colors) */
18
- fontColor = DEFAULT_COLOR_PALETTE[0];
19
- /** The opacity of new text objects (0-1) */
20
- opacity = 1;
13
+ /** Backing field for {@link fontFamily}. */
14
+ _fontFamily = 'Arial';
15
+ /** Backing field for {@link fontSize}. */
16
+ _fontSize = 16;
17
+ /** Backing field for {@link fontColor}. */
18
+ _fontColor = DEFAULT_COLOR_PALETTE[0];
19
+ /** Backing field for {@link opacity}. */
20
+ _opacity = 1;
21
+ /** The font family for new text objects. */
22
+ get fontFamily() { return this._fontFamily; }
23
+ set fontFamily(value) {
24
+ this._fontFamily = value;
25
+ this.applyToActiveText({ fontFamily: value });
26
+ }
27
+ /** The font size for new text objects in pixels. */
28
+ get fontSize() { return this._fontSize; }
29
+ set fontSize(value) {
30
+ this._fontSize = value;
31
+ this.applyToActiveText({ fontSize: value });
32
+ }
33
+ /** The font color for new text objects (supports theme-aware light/dark colors). */
34
+ get fontColor() { return this._fontColor; }
35
+ set fontColor(value) {
36
+ this._fontColor = value;
37
+ this.applyToActiveText({ fontColor: value });
38
+ }
39
+ /** The opacity of new text objects (0-1). */
40
+ get opacity() { return this._opacity; }
41
+ set opacity(value) {
42
+ this._opacity = value;
43
+ this.applyToActiveText({ opacity: value });
44
+ }
21
45
  /** Available color palette for the text tool */
22
46
  palette = [...DEFAULT_COLOR_PALETTE];
23
47
  /**
@@ -27,6 +51,18 @@ export class KritzelTextTool extends KritzelBaseTool {
27
51
  constructor(core) {
28
52
  super(core);
29
53
  }
54
+ /**
55
+ * Propagates a property change to the text object that is currently being edited, if any.
56
+ * Mirrors the selection tool pattern of pushing tool-config changes down to live objects,
57
+ * but scoped to the single text in edit mode rather than the current selection.
58
+ */
59
+ applyToActiveText(updatedProperties) {
60
+ const activeText = this._core?.store?.activeText;
61
+ if (!activeText)
62
+ return;
63
+ this._core.updateObject(activeText, updatedProperties);
64
+ this._core.rerender();
65
+ }
30
66
  /**
31
67
  * Handles pointer down events for text creation and editing.
32
68
  * If clicking on an existing text object, enters edit mode for that object.
@@ -42,7 +78,7 @@ export class KritzelTextTool extends KritzelBaseTool {
42
78
  if (event.pointerType === 'mouse') {
43
79
  const path = event.composedPath().slice(1);
44
80
  const objectElement = path.find(element => element.classList && element.classList.contains('object'));
45
- const object = this._core.findObjectById(objectElement?.id);
81
+ const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
46
82
  const activeText = this._core.store.activeText;
47
83
  if (activeText === null && object instanceof KritzelText) {
48
84
  object.edit(event);
@@ -79,7 +115,7 @@ export class KritzelTextTool extends KritzelBaseTool {
79
115
  const activePointers = Array.from(this._core.store.state.pointers.values());
80
116
  const path = event.composedPath().slice(1);
81
117
  const objectElement = path.find(element => element.classList && element.classList.contains('object'));
82
- const object = this._core.findObjectById(objectElement?.id);
118
+ const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
83
119
  const activeText = this._core.store.activeText;
84
120
  if (activeText === null && object instanceof KritzelText) {
85
121
  object.edit(event);
@@ -112,6 +112,7 @@ export class KritzelEditor {
112
112
  ];
113
113
  objectContextMenuItems = [
114
114
  { label: 'Copy', icon: 'copy', group: 'clipboard', action: () => this.engineRef.copy() },
115
+ { label: 'Cut', icon: 'cut', group: 'clipboard', action: () => this.engineRef.cut() },
115
116
  {
116
117
  label: 'Paste',
117
118
  icon: 'paste',
@@ -372,6 +373,9 @@ export class KritzelEditor {
372
373
  async copy() {
373
374
  return this.engineRef.copy();
374
375
  }
376
+ async cut() {
377
+ return this.engineRef.cut();
378
+ }
375
379
  async paste(x, y) {
376
380
  return this.engineRef.paste(x, y);
377
381
  }
@@ -719,27 +723,27 @@ export class KritzelEditor {
719
723
  const isLoggedIn = this.isLoggedIn;
720
724
  const shouldShowCurrentUser = isLoggedIn;
721
725
  const shouldShowLoginButton = !!this.loginConfig && !isLoggedIn;
722
- return (h(Host, { key: '7c1d6a43480a2daf9574500c7dec54477c5cb844' }, h("div", { key: '6d8f9b10e670f91da433eee2f579c21e0d249892', class: "top-left-buttons" }, h("kritzel-workspace-manager", { key: '3c3a26dae94af3ad404bf81149f0f2311f605563', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), h("kritzel-back-to-content", { key: 'afb425b2c6a4680686de61b9ee17e94d588191bf', visible: this.isBackToContentButtonVisible, onBackToContent: () => this.backToContent() })), h("kritzel-engine", { key: '8258cbb85421ef0cebd34e7c314c1e1f97a76536', ref: el => {
726
+ return (h(Host, { key: '6e832b0036dbc0d36aa70b297cf73cceafbaa846' }, h("div", { key: '48e99e354326781792798fdcc2b07a1b5bbc8cca', class: "top-left-buttons" }, h("kritzel-workspace-manager", { key: '980bf87558891a8b20030be4b6d0c32ee568ca4d', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), h("kritzel-back-to-content", { key: '59636e148d71c54e51f17095dfec2754d63f1692', visible: this.isBackToContentButtonVisible, onBackToContent: () => this.backToContent() })), h("kritzel-engine", { key: '11f17eef609d3d3ece1ead3cf51524e690f4a2eb', ref: el => {
723
727
  if (el) {
724
728
  this.engineRef = el;
725
729
  }
726
- }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, wheelEnabled: this.wheelEnabled, theme: this.currentTheme, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event) }), h("kritzel-controls", { key: '2633c66a89e89ecaa975d28ba6852841be876520', class: { 'keyboard-open': this.isVirtualKeyboardOpen }, style: { display: this.isControlsVisible ? 'flex' : 'none' }, ref: el => {
730
+ }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, wheelEnabled: this.wheelEnabled, theme: this.currentTheme, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event) }), h("kritzel-controls", { key: 'b9852a78ed062ca7026b8d0da44263fb0a64d8cc', class: { 'keyboard-open': this.isVirtualKeyboardOpen }, style: { display: this.isControlsVisible ? 'flex' : 'none' }, ref: el => {
727
731
  if (el) {
728
732
  this.controlsRef = el;
729
733
  }
730
- }, controls: this.controls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.currentTheme, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'd8accf9bdefe7ea3a4a4661380d6dda1c2ecb3ef', class: "top-right-buttons" }, h("kritzel-settings", { key: '00394a7386304c6f7524f38094e8d111f028f56e', ref: el => {
734
+ }, controls: this.controls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.currentTheme, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'f3e33d794033eb5fdbde8c4553c744da61eefc51', class: "top-right-buttons" }, h("kritzel-settings", { key: '466ac8a5d110feb46472c3207f4a73e9ce37e148', ref: el => {
731
735
  if (el) {
732
736
  this.settingsRef = el;
733
737
  }
734
- }, shortcuts: this.shortcuts, editorId: this.editorId, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: '9951b8daff183cd9ce7766f2ab6dfcda927dcd62', ref: el => {
738
+ }, shortcuts: this.shortcuts, editorId: this.editorId, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: 'c6e0eca3baca940f9160ca02989d5c27410ccc59', ref: el => {
735
739
  if (el) {
736
740
  this.exportRef = el;
737
741
  }
738
- }, workspaceName: this.activeWorkspace?.name || 'workspace', onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '7be535beebde53ecb736d06ce2ca54891000ce43', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: '54112b7dcfa5fdaaf86c4eb80cef41898d06e316', user: this.user }), shouldShowLoginButton && h("kritzel-button", { key: '9e9873d348e540a2e104cc061a195f3af7e8ef95', onButtonClick: () => this.loginDialogRef?.open() }, "Sign in"), h("kritzel-more-menu", { key: '9112cf42034a5ee1a059b139bef34f32342ef6dd', items: this.moreMenuItems, visible: this.isMoreMenuVisible }), h("kritzel-share-dialog", { key: '5abcbceb7223960b7cec3e9ca4d848b87c1dec88', ref: el => {
742
+ }, workspaceName: this.activeWorkspace?.name || 'workspace', onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: 'b66a7736b9ac0942873e5bdc1677228cd53d57d6', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: '4ab0b49bd43f428fb1ade5d5d9836d846355d92f', user: this.user }), shouldShowLoginButton && h("kritzel-button", { key: '04d6d43b60ad1bd3272dfb1e09aa3efb9602c42e', onButtonClick: () => this.loginDialogRef?.open() }, "Sign in"), h("kritzel-more-menu", { key: '7ca482736a2253d95dffc369de1db1d14d207c35', items: this.moreMenuItems, visible: this.isMoreMenuVisible }), h("kritzel-share-dialog", { key: 'c5ae3f2e981d980fd35380424036acdc29b0fe53', ref: el => {
739
743
  if (el) {
740
744
  this.shareDialogRef = el;
741
745
  }
742
- }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: 'ee296f1c00215566c26057e8ffb226a9fadc6592', ref: el => {
746
+ }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: 'f72b412715d0ffd9a8038ade192d894623fc98fc', ref: el => {
743
747
  if (el) {
744
748
  this.loginDialogRef = el;
745
749
  }
@@ -1064,7 +1068,7 @@ export class KritzelEditor {
1064
1068
  },
1065
1069
  "getter": false,
1066
1070
  "setter": false,
1067
- "defaultValue": "[\n { label: 'Copy', icon: 'copy', group: 'clipboard', action: () => this.engineRef.copy() },\n {\n label: 'Paste',\n icon: 'paste',\n group: 'clipboard',\n disabled: async () => (await this.engineRef.getCopiedObjects()).length === 0,\n action: (menu, _) => this.engineRef.paste(menu.x, menu.y),\n },\n {\n label: 'Order',\n icon: 'ordering',\n group: 'other',\n children: [\n { label: 'Bring to Front', icon: 'bring-to-front', action: () => this.engineRef.bringToFront() },\n { label: 'Send to Back', icon: 'send-to-back', action: () => this.engineRef.sendToBack() },\n { label: 'Move Up', icon: 'arrow-up-from-dot', action: () => this.engineRef.bringForward() },\n { label: 'Move Down', icon: 'arrow-down-from-dot', action: () => this.engineRef.sendBackward() },\n ],\n },\n {\n label: 'Align',\n icon: 'align',\n group: 'other',\n disabled: async () => (await this.engineRef.getSelectedObjects()).length < 2,\n children: [\n { label: 'Align Left', icon: 'align-start-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.StartHorizontal) },\n { label: 'Align Center Horizontally', icon: 'align-center-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.CenterHorizontal) },\n { label: 'Align Right', icon: 'align-end-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.EndHorizontal) },\n { label: 'Align Top', icon: 'align-start-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.StartVertical) },\n { label: 'Align Center Vertically', icon: 'align-center-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.CenterVertical) },\n { label: 'Align Bottom', icon: 'align-end-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.EndVertical) },\n ],\n },\n {\n label: 'Group',\n icon: 'group',\n group: 'other',\n children: [\n {\n label: 'Group',\n icon: 'group',\n disabled: async () => (await this.engineRef.getSelectedObjects()).length < 2,\n action: () => this.engineRef.group(),\n },\n {\n label: 'Ungroup',\n icon: 'ungroup',\n disabled: async () => {\n const selectedObjects = await this.engineRef.getSelectedObjects();\n return !selectedObjects.some(obj => obj.__class__ === 'KritzelGroup');\n },\n action: () => this.engineRef.ungroup(),\n },\n ],\n },\n {\n label: 'Export',\n icon: 'download',\n group: 'export',\n children: [\n { label: 'Export as SVG', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsSvg() },\n { label: 'Export as PNG', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsPng() },\n ],\n },\n { label: 'Delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },\n ]"
1071
+ "defaultValue": "[\n { label: 'Copy', icon: 'copy', group: 'clipboard', action: () => this.engineRef.copy() },\n { label: 'Cut', icon: 'cut', group: 'clipboard', action: () => this.engineRef.cut() },\n {\n label: 'Paste',\n icon: 'paste',\n group: 'clipboard',\n disabled: async () => (await this.engineRef.getCopiedObjects()).length === 0,\n action: (menu, _) => this.engineRef.paste(menu.x, menu.y),\n },\n {\n label: 'Order',\n icon: 'ordering',\n group: 'other',\n children: [\n { label: 'Bring to Front', icon: 'bring-to-front', action: () => this.engineRef.bringToFront() },\n { label: 'Send to Back', icon: 'send-to-back', action: () => this.engineRef.sendToBack() },\n { label: 'Move Up', icon: 'arrow-up-from-dot', action: () => this.engineRef.bringForward() },\n { label: 'Move Down', icon: 'arrow-down-from-dot', action: () => this.engineRef.sendBackward() },\n ],\n },\n {\n label: 'Align',\n icon: 'align',\n group: 'other',\n disabled: async () => (await this.engineRef.getSelectedObjects()).length < 2,\n children: [\n { label: 'Align Left', icon: 'align-start-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.StartHorizontal) },\n { label: 'Align Center Horizontally', icon: 'align-center-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.CenterHorizontal) },\n { label: 'Align Right', icon: 'align-end-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.EndHorizontal) },\n { label: 'Align Top', icon: 'align-start-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.StartVertical) },\n { label: 'Align Center Vertically', icon: 'align-center-vertical', action: () => this.engineRef.alignObjects(KritzelAlignment.CenterVertical) },\n { label: 'Align Bottom', icon: 'align-end-horizontal', action: () => this.engineRef.alignObjects(KritzelAlignment.EndVertical) },\n ],\n },\n {\n label: 'Group',\n icon: 'group',\n group: 'other',\n children: [\n {\n label: 'Group',\n icon: 'group',\n disabled: async () => (await this.engineRef.getSelectedObjects()).length < 2,\n action: () => this.engineRef.group(),\n },\n {\n label: 'Ungroup',\n icon: 'ungroup',\n disabled: async () => {\n const selectedObjects = await this.engineRef.getSelectedObjects();\n return !selectedObjects.some(obj => obj.__class__ === 'KritzelGroup');\n },\n action: () => this.engineRef.ungroup(),\n },\n ],\n },\n {\n label: 'Export',\n icon: 'download',\n group: 'export',\n children: [\n { label: 'Export as SVG', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsSvg() },\n { label: 'Export as PNG', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsPng() },\n ],\n },\n { label: 'Delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },\n ]"
1068
1072
  },
1069
1073
  "customSvgIcons": {
1070
1074
  "type": "unknown",
@@ -2382,6 +2386,23 @@ export class KritzelEditor {
2382
2386
  "tags": []
2383
2387
  }
2384
2388
  },
2389
+ "cut": {
2390
+ "complexType": {
2391
+ "signature": "() => Promise<void>",
2392
+ "parameters": [],
2393
+ "references": {
2394
+ "Promise": {
2395
+ "location": "global",
2396
+ "id": "global::Promise"
2397
+ }
2398
+ },
2399
+ "return": "Promise<void>"
2400
+ },
2401
+ "docs": {
2402
+ "text": "",
2403
+ "tags": []
2404
+ }
2405
+ },
2385
2406
  "paste": {
2386
2407
  "complexType": {
2387
2408
  "signature": "(x: number, y: number) => Promise<void>",
@@ -1,6 +1,8 @@
1
1
  import { Host, h } from "@stencil/core";
2
2
  import { KritzelViewport } from "../../../classes/core/viewport.class";
3
3
  import { KritzelSelectionTool } from "../../../classes/tools/selection-tool.class";
4
+ import { KritzelTextTool } from "../../../classes/tools/text-tool.class";
5
+ import { KritzelShapeTool } from "../../../classes/tools/shape-tool.class";
4
6
  import { KritzelSelectionBox } from "../../../classes/objects/selection-box.class";
5
7
  import { KritzelKeyHandler } from "../../../classes/handlers/key.handler";
6
8
  import { KritzelBaseTool } from "../../../classes/tools/base-tool.class";
@@ -396,6 +398,10 @@ export class KritzelEngine {
396
398
  async copy() {
397
399
  this.core.copy();
398
400
  }
401
+ /** Cuts the currently selected objects to the internal clipboard (deletes them from the canvas). */
402
+ async cut() {
403
+ this.core.cut();
404
+ }
399
405
  /**
400
406
  * Pastes previously copied objects at the specified world coordinates.
401
407
  * @param x - X position in world coordinates.
@@ -525,23 +531,39 @@ export class KritzelEngine {
525
531
  object.setContent(pendingContent);
526
532
  }
527
533
  }
528
- // Handle KritzelGroup: flush pending children into the store
534
+ // Handle KritzelGroup: flush pending children (recursively for nested groups) into the store
529
535
  if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup') && object._pendingChildren.length > 0) {
530
- object.childIds = [];
531
- // Build a map of old ID new ID so we can remap anchor references
536
+ // Build a map of old ID → new ID across all nesting levels so we can
537
+ // remap anchor references regardless of how deeply nested the line is.
532
538
  const idRemapping = new Map();
533
- object._pendingChildren.forEach(child => {
534
- const oldId = child.id;
535
- child.id = child.generateId();
536
- child._core = this.core;
537
- child.scale = this.core.store.state.scale;
538
- child.zIndex = this.core.store.currentZIndex;
539
- child.workspaceId = this.core.store.state.activeWorkspace.id;
540
- idRemapping.set(oldId, child.id);
541
- object.childIds.push(child.id);
542
- });
543
- // Remap anchor references in lines before inserting into the store
544
- object._pendingChildren.forEach(child => {
539
+ const allFlushedChildren = [];
540
+ const flushGroup = (group) => {
541
+ if (group._pendingChildren.length === 0) {
542
+ return;
543
+ }
544
+ const pending = group._pendingChildren;
545
+ group._pendingChildren = [];
546
+ group.childIds = [];
547
+ pending.forEach(child => {
548
+ const oldId = child.id;
549
+ child.id = child.generateId();
550
+ child._core = this.core;
551
+ child.scale = this.core.store.state.scale;
552
+ child.zIndex = this.core.store.currentZIndex;
553
+ child.workspaceId = this.core.store.state.activeWorkspace.id;
554
+ idRemapping.set(oldId, child.id);
555
+ group.childIds.push(child.id);
556
+ allFlushedChildren.push(child);
557
+ // Recurse into nested groups before inserting so grandchildren get
558
+ // proper IDs and `childIds` references before the parent is added.
559
+ if (KritzelClassHelper.isInstanceOf(child, 'KritzelGroup')) {
560
+ flushGroup(child);
561
+ }
562
+ });
563
+ };
564
+ flushGroup(object);
565
+ // Remap anchor references in lines across all flushed descendants
566
+ allFlushedChildren.forEach(child => {
545
567
  if (KritzelClassHelper.isInstanceOf(child, 'KritzelLine')) {
546
568
  if (child.startAnchor && idRemapping.has(child.startAnchor.objectId)) {
547
569
  child.startAnchor = { objectId: idRemapping.get(child.startAnchor.objectId) };
@@ -551,10 +573,11 @@ export class KritzelEngine {
551
573
  }
552
574
  }
553
575
  });
554
- object._pendingChildren.forEach(child => {
576
+ // Insert all flushed descendants into the store
577
+ allFlushedChildren.forEach(child => {
555
578
  this.core.addObject(child);
556
579
  });
557
- object._pendingChildren = [];
580
+ // finalize() recurses into nested groups so all bounding boxes/snapshots align
558
581
  object.finalize();
559
582
  // Rebuild anchor index so anchored lines are tracked
560
583
  this.core.anchorManager.rebuildIndex();
@@ -1494,6 +1517,12 @@ export class KritzelEngine {
1494
1517
  this.core.store.state.isResizeHandleSelected = false;
1495
1518
  this.core.store.state.isRotationHandleSelected = false;
1496
1519
  }
1520
+ if (!(activeTool instanceof KritzelTextTool)) {
1521
+ this.core.resetActiveText();
1522
+ }
1523
+ if (!(activeTool instanceof KritzelShapeTool)) {
1524
+ this.core.resetActiveShape();
1525
+ }
1497
1526
  this.core.store.state.skipContextMenu = false;
1498
1527
  this.core.store.state.copiedObjects = undefined;
1499
1528
  if (activeTool) {
@@ -1570,7 +1599,7 @@ export class KritzelEngine {
1570
1599
  overflow: 'visible',
1571
1600
  userSelect: 'none',
1572
1601
  imageRendering: this.core.store.state.isScaling || this.core.store.state.isPanning ? 'pixelated' : 'auto',
1573
- }, draggable: false, onDragStart: e => e.preventDefault() })), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState !== 'ready' && (h("div", { ref: () => object.ensureLoaded(), style: {
1602
+ }, draggable: false, decoding: "async", loading: "eager", onDragStart: e => e.preventDefault() })), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState !== 'ready' && (h("div", { ref: () => object.ensureLoaded(), style: {
1574
1603
  position: 'absolute',
1575
1604
  left: '0',
1576
1605
  top: '0',
@@ -2897,6 +2926,23 @@ export class KritzelEngine {
2897
2926
  "tags": []
2898
2927
  }
2899
2928
  },
2929
+ "cut": {
2930
+ "complexType": {
2931
+ "signature": "() => Promise<void>",
2932
+ "parameters": [],
2933
+ "references": {
2934
+ "Promise": {
2935
+ "location": "global",
2936
+ "id": "global::Promise"
2937
+ }
2938
+ },
2939
+ "return": "Promise<void>"
2940
+ },
2941
+ "docs": {
2942
+ "text": "Cuts the currently selected objects to the internal clipboard (deletes them from the canvas).",
2943
+ "tags": []
2944
+ }
2945
+ },
2900
2946
  "paste": {
2901
2947
  "complexType": {
2902
2948
  "signature": "(x: number, y: number) => Promise<void>",
@@ -40,6 +40,22 @@ export class KritzelPortal {
40
40
  this.closePortal();
41
41
  }
42
42
  }
43
+ handleOutsidePointerDown(event) {
44
+ if (!this.portal)
45
+ return;
46
+ const isLastPortal = this.lastAddedPortal === this.portal;
47
+ if (!isLastPortal)
48
+ return;
49
+ const path = event.composedPath();
50
+ const isInsidePortal = path.some(el => el === this.host);
51
+ const isOnAnchor = this.anchor && path.some(el => el === this.anchor);
52
+ if (!isInsidePortal && !isOnAnchor) {
53
+ event.stopPropagation();
54
+ event.preventDefault();
55
+ this.close.emit();
56
+ this.closePortal();
57
+ }
58
+ }
43
59
  handleKeyDown(event) {
44
60
  const isLastPortal = this.lastAddedPortal === this.portal;
45
61
  if (!isLastPortal)
@@ -261,7 +277,7 @@ export class KritzelPortal {
261
277
  this.portal.style.visibility = 'visible';
262
278
  }
263
279
  render() {
264
- return (h(Host, { key: '6acf9d402aa0ee151d93b912fc2b9d46cfe88d07', style: { display: this.anchor ? 'block' : 'none' } }, h("slot", { key: 'dbeff1735d422e818179346f5880555fbadddba9' })));
280
+ return (h(Host, { key: 'dcd8e6f3787c713012aeb6436bf63f2f4930c39e', style: { display: this.anchor ? 'block' : 'none' } }, h("slot", { key: '830f51521a77f0a28471026494323e14ab4f9f9a' })));
265
281
  }
266
282
  static get is() { return "kritzel-portal"; }
267
283
  static get encapsulation() { return "shadow"; }
@@ -387,6 +403,12 @@ export class KritzelPortal {
387
403
  "target": "window",
388
404
  "capture": false,
389
405
  "passive": false
406
+ }, {
407
+ "name": "pointerdown",
408
+ "method": "handleOutsidePointerDown",
409
+ "target": "document",
410
+ "capture": true,
411
+ "passive": false
390
412
  }, {
391
413
  "name": "keydown",
392
414
  "method": "handleKeyDown",
@@ -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.0';
6
+ export const KRITZEL_VERSION = '0.2.2';
@@ -117,6 +117,11 @@ export const darkTheme = {
117
117
  engine: {
118
118
  backgroundColor: '#1a1a1a',
119
119
  },
120
+ snap: {
121
+ indicatorFill: 'rgba(10, 132, 255, 0.35)',
122
+ indicatorStroke: '#0A84FF',
123
+ lineStroke: 'rgba(255, 255, 255, 0.35)',
124
+ },
120
125
  fontSize: {
121
126
  hoverBackgroundColor: '#3a3a3a',
122
127
  selectedBackgroundColor: '#3a3a3a',
@@ -118,6 +118,11 @@ export const lightTheme = {
118
118
  engine: {
119
119
  backgroundColor: '#ffffff',
120
120
  },
121
+ snap: {
122
+ indicatorFill: 'rgba(59, 130, 246, 0.3)',
123
+ indicatorStroke: '#007bff',
124
+ lineStroke: 'rgba(0, 0, 0, 0.2)',
125
+ },
121
126
  fontSize: {
122
127
  hoverBackgroundColor: '#ebebeb',
123
128
  selectedBackgroundColor: '#ebebeb',