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.
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/kritzel-active-users_42.cjs.entry.js +177 -52
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/stencil.cjs.js +1 -1
- package/dist/cjs/{workspace.migrations-CPncKohl.js → workspace.migrations-BPwtowiJ.js} +208 -24
- package/dist/collection/classes/core/core.class.js +27 -19
- package/dist/collection/classes/core/viewport.class.js +3 -0
- package/dist/collection/classes/handlers/key.handler.js +73 -6
- package/dist/collection/classes/handlers/selection.handler.js +36 -2
- package/dist/collection/classes/objects/group.class.js +69 -12
- package/dist/collection/classes/objects/shape.class.js +24 -0
- package/dist/collection/classes/objects/text.class.js +23 -0
- package/dist/collection/classes/tools/text-tool.class.js +46 -10
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +28 -7
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +64 -18
- package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +23 -1
- package/dist/collection/constants/version.js +1 -1
- package/dist/collection/themes/dark-theme.js +5 -0
- package/dist/collection/themes/light-theme.js +5 -0
- package/dist/components/index.js +1 -1
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-color-palette.js +1 -1
- package/dist/components/kritzel-color.js +1 -1
- package/dist/components/kritzel-controls.js +1 -1
- package/dist/components/kritzel-editor.js +1 -1
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-menu-item.js +1 -1
- package/dist/components/kritzel-menu.js +1 -1
- package/dist/components/kritzel-more-menu.js +1 -1
- package/dist/components/kritzel-portal.js +1 -1
- package/dist/components/kritzel-settings.js +1 -1
- package/dist/components/kritzel-split-button.js +1 -1
- package/dist/components/kritzel-stroke-size.js +1 -1
- package/dist/components/kritzel-tool-config.js +1 -1
- package/dist/components/kritzel-workspace-manager.js +1 -1
- package/dist/components/{p-CFzvz-B2.js → p-0YBCp8Wh.js} +1 -1
- package/dist/components/{p-DkT0CXfN.js → p-574MVXxi.js} +1 -1
- package/dist/components/p-BCzbwL4m.js +1 -0
- package/dist/components/p-BLjdzUzs.js +1 -0
- package/dist/components/{p-BFQVg_eQ.js → p-BSEdLfq2.js} +1 -1
- package/dist/components/{p-C3Dwuqka.js → p-BWrxz4mM.js} +1 -1
- package/dist/components/{p-ChqeIKg_.js → p-BYOIzv_f.js} +1 -1
- package/dist/components/{p-CekG3_ce.js → p-Bfa-Amjn.js} +1 -1
- package/dist/components/{p-9ASFIqd0.js → p-BmcAX-1k.js} +1 -1
- package/dist/components/{p-CzIuqMQA.js → p-BtJB7FsW.js} +1 -1
- package/dist/components/{p-C_yfHS4F.js → p-C6Td7I4k.js} +1 -1
- package/dist/components/{p-ChQNi67Z.js → p-D9ifYAtg.js} +1 -1
- package/dist/components/{p-B_rHzy0t.js → p-DE2xDwUM.js} +1 -1
- package/dist/components/{p-CaDBSaxZ.js → p-DFeyobdy.js} +2 -2
- package/dist/components/{p-BHSRRiEg.js → p-DfB7uJ0N.js} +1 -1
- package/dist/components/{p-B2kHVHa_.js → p-u-827ZX7.js} +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/kritzel-active-users_42.entry.js +177 -52
- package/dist/esm/loader.js +1 -1
- package/dist/esm/stencil.js +1 -1
- package/dist/esm/{workspace.migrations-ytjzXm9B.js → workspace.migrations-C_uxbvuH.js} +208 -24
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-4d28c496.entry.js +9 -0
- package/dist/stencil/p-C_uxbvuH.js +1 -0
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/handlers/selection.handler.d.ts +15 -0
- package/dist/types/classes/objects/group.class.d.ts +15 -0
- package/dist/types/classes/objects/shape.class.d.ts +10 -0
- package/dist/types/classes/objects/text.class.d.ts +9 -0
- package/dist/types/classes/tools/text-tool.class.d.ts +26 -8
- package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +1 -0
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +2 -0
- package/dist/types/components/shared/kritzel-portal/kritzel-portal.d.ts +1 -0
- package/dist/types/components.d.ts +7 -2
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/interfaces/theme.interface.d.ts +12 -4
- package/package.json +1 -1
- package/dist/components/p-BYX50YSd.js +0 -1
- package/dist/components/p-CjazGGq3.js +0 -1
- package/dist/stencil/p-19e04f32.entry.js +0 -9
- package/dist/stencil/p-ytjzXm9B.js +0 -1
|
@@ -14512,6 +14512,11 @@ const lightTheme = {
|
|
|
14512
14512
|
engine: {
|
|
14513
14513
|
backgroundColor: '#ffffff',
|
|
14514
14514
|
},
|
|
14515
|
+
snap: {
|
|
14516
|
+
indicatorFill: 'rgba(59, 130, 246, 0.3)',
|
|
14517
|
+
indicatorStroke: '#007bff',
|
|
14518
|
+
lineStroke: 'rgba(0, 0, 0, 0.2)',
|
|
14519
|
+
},
|
|
14515
14520
|
fontSize: {
|
|
14516
14521
|
hoverBackgroundColor: '#ebebeb',
|
|
14517
14522
|
selectedBackgroundColor: '#ebebeb',
|
|
@@ -14776,6 +14781,11 @@ const darkTheme = {
|
|
|
14776
14781
|
engine: {
|
|
14777
14782
|
backgroundColor: '#1a1a1a',
|
|
14778
14783
|
},
|
|
14784
|
+
snap: {
|
|
14785
|
+
indicatorFill: 'rgba(10, 132, 255, 0.35)',
|
|
14786
|
+
indicatorStroke: '#0A84FF',
|
|
14787
|
+
lineStroke: 'rgba(255, 255, 255, 0.35)',
|
|
14788
|
+
},
|
|
14779
14789
|
fontSize: {
|
|
14780
14790
|
hoverBackgroundColor: '#3a3a3a',
|
|
14781
14791
|
selectedBackgroundColor: '#3a3a3a',
|
|
@@ -15568,6 +15578,29 @@ class KritzelText extends KritzelBaseObject {
|
|
|
15568
15578
|
this._core.store.objects.update(this);
|
|
15569
15579
|
this._core.engine.emitObjectsChange();
|
|
15570
15580
|
}
|
|
15581
|
+
/**
|
|
15582
|
+
* Handles the Escape key while the text object is in edit mode.
|
|
15583
|
+
* Implements a two-stage behavior: if the editor currently has a non-empty
|
|
15584
|
+
* text selection, the selection is collapsed to a caret without exiting
|
|
15585
|
+
* edit mode. If the selection is already collapsed, edit mode is exited
|
|
15586
|
+
* (saving the current content or deleting the object if it is empty) and
|
|
15587
|
+
* the canvas selection is cleared so subsequent tool actions start fresh.
|
|
15588
|
+
*/
|
|
15589
|
+
handleEscape() {
|
|
15590
|
+
if (!this.editor || !this.isEditing) {
|
|
15591
|
+
return;
|
|
15592
|
+
}
|
|
15593
|
+
const { state } = this.editor;
|
|
15594
|
+
if (!state.selection.empty) {
|
|
15595
|
+
const pos = state.selection.head;
|
|
15596
|
+
this.editor.dispatch(state.tr.setSelection(TextSelection.create(state.doc, pos)));
|
|
15597
|
+
this.editor.focus();
|
|
15598
|
+
return;
|
|
15599
|
+
}
|
|
15600
|
+
this._core.resetActiveText();
|
|
15601
|
+
this._core.clearSelection();
|
|
15602
|
+
this._core.store.setState('activeTool', KritzelToolRegistry.getTool('selection'));
|
|
15603
|
+
}
|
|
15571
15604
|
/**
|
|
15572
15605
|
* Handles pointer down events during text editing.
|
|
15573
15606
|
* Stops event propagation to prevent canvas interaction while editing.
|
|
@@ -18245,6 +18278,23 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18245
18278
|
}
|
|
18246
18279
|
return null;
|
|
18247
18280
|
}
|
|
18281
|
+
/**
|
|
18282
|
+
* Recursively collects the IDs of all descendants of the given group (children,
|
|
18283
|
+
* grandchildren, etc.). Useful for detecting when a selection contains both an
|
|
18284
|
+
* ancestor group and one of its descendants — a configuration that would otherwise
|
|
18285
|
+
* cause the descendant to be transformed twice (once via the ancestor, once directly).
|
|
18286
|
+
* @param group - The group whose descendants should be collected.
|
|
18287
|
+
* @param out - A set that will be populated with all descendant object IDs.
|
|
18288
|
+
*/
|
|
18289
|
+
static collectDescendantIds(group, out = new Set()) {
|
|
18290
|
+
for (const child of group.children) {
|
|
18291
|
+
out.add(child.id);
|
|
18292
|
+
if (child instanceof KritzelGroup) {
|
|
18293
|
+
KritzelGroup.collectDescendantIds(child, out);
|
|
18294
|
+
}
|
|
18295
|
+
}
|
|
18296
|
+
return out;
|
|
18297
|
+
}
|
|
18248
18298
|
/**
|
|
18249
18299
|
* Adds a child object to this group.
|
|
18250
18300
|
* If the object is already a child, no action is taken.
|
|
@@ -18288,8 +18338,19 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18288
18338
|
* Finalizes the group after children have been positioned (e.g., after paste).
|
|
18289
18339
|
* Refreshes the bounding box to encompass all children and captures
|
|
18290
18340
|
* child snapshots for subsequent transformation operations.
|
|
18341
|
+
*
|
|
18342
|
+
* Recursively finalizes nested child groups first, so that when a transform
|
|
18343
|
+
* (rotate/resize) cascades through nested groups, every group's snapshots are
|
|
18344
|
+
* aligned with the current visual state. Without this, a nested group's
|
|
18345
|
+
* `snapshotRotation` could be stale, causing the inner group to be offset
|
|
18346
|
+
* relative to its parent during rotation.
|
|
18291
18347
|
*/
|
|
18292
18348
|
finalize() {
|
|
18349
|
+
for (const child of this.children) {
|
|
18350
|
+
if (child instanceof KritzelGroup) {
|
|
18351
|
+
child.finalize();
|
|
18352
|
+
}
|
|
18353
|
+
}
|
|
18293
18354
|
this.refreshBoundingBox();
|
|
18294
18355
|
this.captureChildSnapshots();
|
|
18295
18356
|
}
|
|
@@ -18446,11 +18507,23 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18446
18507
|
* @param height - The new height of the group's content area.
|
|
18447
18508
|
*/
|
|
18448
18509
|
resize(x, y, width, height) {
|
|
18449
|
-
|
|
18450
|
-
|
|
18451
|
-
//
|
|
18452
|
-
const
|
|
18453
|
-
|
|
18510
|
+
// Use snapshot dimensions (stable across the gesture) instead of `this.*`,
|
|
18511
|
+
// which mutates each frame and gets distorted by descendants like KritzelText
|
|
18512
|
+
// that clamp to a uniform scale, causing the cascade factor to drift.
|
|
18513
|
+
const baseWidth = this.snapshotTotalWidth > 0
|
|
18514
|
+
? this.snapshotTotalWidth - this.padding * 2
|
|
18515
|
+
: this.width;
|
|
18516
|
+
const baseHeight = this.snapshotTotalHeight > 0
|
|
18517
|
+
? this.snapshotTotalHeight - this.padding * 2
|
|
18518
|
+
: this.height;
|
|
18519
|
+
const baseScale = this.snapshotScale || this.scale || 1;
|
|
18520
|
+
const widthScaleFactor = baseWidth !== 0 ? width / baseWidth : 1;
|
|
18521
|
+
const heightScaleFactor = baseHeight !== 0 ? height / baseHeight : 1;
|
|
18522
|
+
// Calculate old center from snapshot (stable across the gesture)
|
|
18523
|
+
const snapshotTotalWidth = this.snapshotTotalWidth || (this.width + this.padding * 2);
|
|
18524
|
+
const snapshotTotalHeight = this.snapshotTotalHeight || (this.height + this.padding * 2);
|
|
18525
|
+
const oldCenterX = this.snapshotTranslateX + snapshotTotalWidth / 2 / baseScale;
|
|
18526
|
+
const oldCenterY = this.snapshotTranslateY + snapshotTotalHeight / 2 / baseScale;
|
|
18454
18527
|
// Calculate new center
|
|
18455
18528
|
const newTotalWidth = width + this.padding * 2;
|
|
18456
18529
|
const newTotalHeight = height + this.padding * 2;
|
|
@@ -18463,9 +18536,20 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18463
18536
|
const sinR = Math.sin(rotation);
|
|
18464
18537
|
this._core.store.objects.transaction(() => {
|
|
18465
18538
|
this.children.forEach(child => {
|
|
18466
|
-
//
|
|
18467
|
-
|
|
18468
|
-
const
|
|
18539
|
+
// Use snapshot values for the child to avoid compounding distortions caused
|
|
18540
|
+
// by descendants like text that cannot honour requested aspect ratios.
|
|
18541
|
+
const snapshot = this.unchangedChildSnapshots.get(child.id);
|
|
18542
|
+
const childTranslateX = snapshot ? snapshot.translateX : child.translateX;
|
|
18543
|
+
const childTranslateY = snapshot ? snapshot.translateY : child.translateY;
|
|
18544
|
+
const childTotalWidth = snapshot ? snapshot.totalWidth : child.totalWidth;
|
|
18545
|
+
const childTotalHeight = snapshot ? snapshot.totalHeight : child.totalHeight;
|
|
18546
|
+
const childWidth = snapshot ? snapshot.width : child.width;
|
|
18547
|
+
const childHeight = snapshot ? snapshot.height : child.height;
|
|
18548
|
+
const childRotation = snapshot ? snapshot.rotation : child.rotation;
|
|
18549
|
+
const childScale = (snapshot ? snapshot.scale : child.scale) || 1;
|
|
18550
|
+
// Calculate child center from snapshot
|
|
18551
|
+
const childCenterX = childTranslateX + childTotalWidth / 2 / childScale;
|
|
18552
|
+
const childCenterY = childTranslateY + childTotalHeight / 2 / childScale;
|
|
18469
18553
|
// Vector from old group center to child center
|
|
18470
18554
|
const dx = childCenterX - oldCenterX;
|
|
18471
18555
|
const dy = childCenterY - oldCenterY;
|
|
@@ -18482,13 +18566,13 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18482
18566
|
const newChildCenterX = newCenterX + rotatedX;
|
|
18483
18567
|
const newChildCenterY = newCenterY + rotatedY;
|
|
18484
18568
|
// Calculate relative rotation for scaling
|
|
18485
|
-
const relativeRotation =
|
|
18569
|
+
const relativeRotation = childRotation - rotation;
|
|
18486
18570
|
const cosRel = Math.cos(relativeRotation);
|
|
18487
18571
|
const sinRel = Math.sin(relativeRotation);
|
|
18488
18572
|
const newChildWidthScale = Math.sqrt(Math.pow(widthScaleFactor * cosRel, 2) + Math.pow(heightScaleFactor * sinRel, 2));
|
|
18489
18573
|
const newChildHeightScale = Math.sqrt(Math.pow(widthScaleFactor * sinRel, 2) + Math.pow(heightScaleFactor * cosRel, 2));
|
|
18490
|
-
const updatedWidth =
|
|
18491
|
-
const updatedHeight =
|
|
18574
|
+
const updatedWidth = childWidth * newChildWidthScale;
|
|
18575
|
+
const updatedHeight = childHeight * newChildHeightScale;
|
|
18492
18576
|
const updatedTotalWidth = updatedWidth + child.padding * 2;
|
|
18493
18577
|
const updatedTotalHeight = updatedHeight + child.padding * 2;
|
|
18494
18578
|
const updatedX = newChildCenterX - updatedTotalWidth / 2 / child.scale;
|
|
@@ -18498,7 +18582,13 @@ class KritzelGroup extends KritzelBaseObject {
|
|
|
18498
18582
|
this._core.anchorManager.updateAnchorsForObject(child.id);
|
|
18499
18583
|
});
|
|
18500
18584
|
this.refreshBoundingBox();
|
|
18501
|
-
|
|
18585
|
+
// Don't recapture snapshots while an interactive resize is in progress —
|
|
18586
|
+
// recapturing each frame would defeat the purpose of using stable snapshots
|
|
18587
|
+
// and re-introduce the compounding distortion described above. Snapshots are
|
|
18588
|
+
// refreshed at the start of the next gesture via beginTransform/finalize.
|
|
18589
|
+
if (!this._core.store.state.isResizing) {
|
|
18590
|
+
this.captureChildSnapshots();
|
|
18591
|
+
}
|
|
18502
18592
|
this._core.store.objects.update(this);
|
|
18503
18593
|
});
|
|
18504
18594
|
}
|
|
@@ -18997,6 +19087,30 @@ class KritzelShape extends KritzelBaseObject {
|
|
|
18997
19087
|
this._core.store.objects.update(this);
|
|
18998
19088
|
this._core.engine.emitObjectsChange();
|
|
18999
19089
|
}
|
|
19090
|
+
/**
|
|
19091
|
+
* Handles the Escape key while the shape's text editor is in edit mode.
|
|
19092
|
+
* Implements a two-stage behavior: if the editor currently has a non-empty
|
|
19093
|
+
* text selection, the selection is collapsed to a caret without exiting
|
|
19094
|
+
* edit mode. If the selection is already collapsed, edit mode is exited
|
|
19095
|
+
* (saving the current content), the canvas selection is cleared and the
|
|
19096
|
+
* active tool is switched back to the selection tool so subsequent
|
|
19097
|
+
* interactions start fresh.
|
|
19098
|
+
*/
|
|
19099
|
+
handleEscape() {
|
|
19100
|
+
if (!this.editor || !this.isEditing) {
|
|
19101
|
+
return;
|
|
19102
|
+
}
|
|
19103
|
+
const { state } = this.editor;
|
|
19104
|
+
if (!state.selection.empty) {
|
|
19105
|
+
const pos = state.selection.head;
|
|
19106
|
+
this.editor.dispatch(state.tr.setSelection(TextSelection.create(state.doc, pos)));
|
|
19107
|
+
this.editor.focus();
|
|
19108
|
+
return;
|
|
19109
|
+
}
|
|
19110
|
+
this._core.resetActiveShape();
|
|
19111
|
+
this._core.clearSelection();
|
|
19112
|
+
this._core.store.setState('activeTool', KritzelToolRegistry.getTool('selection'));
|
|
19113
|
+
}
|
|
19000
19114
|
/**
|
|
19001
19115
|
* Handles pointer down events when the shape is in edit mode.
|
|
19002
19116
|
* This method stops event propagation for untracked pointers to prevent
|
|
@@ -20770,14 +20884,38 @@ class KritzelImageTool extends KritzelBaseTool {
|
|
|
20770
20884
|
* Supports configurable font family, size, color, and opacity.
|
|
20771
20885
|
*/
|
|
20772
20886
|
class KritzelTextTool extends KritzelBaseTool {
|
|
20773
|
-
/**
|
|
20774
|
-
|
|
20775
|
-
/**
|
|
20776
|
-
|
|
20777
|
-
/**
|
|
20778
|
-
|
|
20779
|
-
/**
|
|
20780
|
-
|
|
20887
|
+
/** Backing field for {@link fontFamily}. */
|
|
20888
|
+
_fontFamily = 'Arial';
|
|
20889
|
+
/** Backing field for {@link fontSize}. */
|
|
20890
|
+
_fontSize = 16;
|
|
20891
|
+
/** Backing field for {@link fontColor}. */
|
|
20892
|
+
_fontColor = DEFAULT_COLOR_PALETTE[0];
|
|
20893
|
+
/** Backing field for {@link opacity}. */
|
|
20894
|
+
_opacity = 1;
|
|
20895
|
+
/** The font family for new text objects. */
|
|
20896
|
+
get fontFamily() { return this._fontFamily; }
|
|
20897
|
+
set fontFamily(value) {
|
|
20898
|
+
this._fontFamily = value;
|
|
20899
|
+
this.applyToActiveText({ fontFamily: value });
|
|
20900
|
+
}
|
|
20901
|
+
/** The font size for new text objects in pixels. */
|
|
20902
|
+
get fontSize() { return this._fontSize; }
|
|
20903
|
+
set fontSize(value) {
|
|
20904
|
+
this._fontSize = value;
|
|
20905
|
+
this.applyToActiveText({ fontSize: value });
|
|
20906
|
+
}
|
|
20907
|
+
/** The font color for new text objects (supports theme-aware light/dark colors). */
|
|
20908
|
+
get fontColor() { return this._fontColor; }
|
|
20909
|
+
set fontColor(value) {
|
|
20910
|
+
this._fontColor = value;
|
|
20911
|
+
this.applyToActiveText({ fontColor: value });
|
|
20912
|
+
}
|
|
20913
|
+
/** The opacity of new text objects (0-1). */
|
|
20914
|
+
get opacity() { return this._opacity; }
|
|
20915
|
+
set opacity(value) {
|
|
20916
|
+
this._opacity = value;
|
|
20917
|
+
this.applyToActiveText({ opacity: value });
|
|
20918
|
+
}
|
|
20781
20919
|
/** Available color palette for the text tool */
|
|
20782
20920
|
palette = [...DEFAULT_COLOR_PALETTE];
|
|
20783
20921
|
/**
|
|
@@ -20787,6 +20925,18 @@ class KritzelTextTool extends KritzelBaseTool {
|
|
|
20787
20925
|
constructor(core) {
|
|
20788
20926
|
super(core);
|
|
20789
20927
|
}
|
|
20928
|
+
/**
|
|
20929
|
+
* Propagates a property change to the text object that is currently being edited, if any.
|
|
20930
|
+
* Mirrors the selection tool pattern of pushing tool-config changes down to live objects,
|
|
20931
|
+
* but scoped to the single text in edit mode rather than the current selection.
|
|
20932
|
+
*/
|
|
20933
|
+
applyToActiveText(updatedProperties) {
|
|
20934
|
+
const activeText = this._core?.store?.activeText;
|
|
20935
|
+
if (!activeText)
|
|
20936
|
+
return;
|
|
20937
|
+
this._core.updateObject(activeText, updatedProperties);
|
|
20938
|
+
this._core.rerender();
|
|
20939
|
+
}
|
|
20790
20940
|
/**
|
|
20791
20941
|
* Handles pointer down events for text creation and editing.
|
|
20792
20942
|
* If clicking on an existing text object, enters edit mode for that object.
|
|
@@ -20802,7 +20952,7 @@ class KritzelTextTool extends KritzelBaseTool {
|
|
|
20802
20952
|
if (event.pointerType === 'mouse') {
|
|
20803
20953
|
const path = event.composedPath().slice(1);
|
|
20804
20954
|
const objectElement = path.find(element => element.classList && element.classList.contains('object'));
|
|
20805
|
-
const object = this._core.findObjectById(objectElement
|
|
20955
|
+
const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
|
|
20806
20956
|
const activeText = this._core.store.activeText;
|
|
20807
20957
|
if (activeText === null && object instanceof KritzelText) {
|
|
20808
20958
|
object.edit(event);
|
|
@@ -20839,7 +20989,7 @@ class KritzelTextTool extends KritzelBaseTool {
|
|
|
20839
20989
|
const activePointers = Array.from(this._core.store.state.pointers.values());
|
|
20840
20990
|
const path = event.composedPath().slice(1);
|
|
20841
20991
|
const objectElement = path.find(element => element.classList && element.classList.contains('object'));
|
|
20842
|
-
const object = this._core.findObjectById(objectElement
|
|
20992
|
+
const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
|
|
20843
20993
|
const activeText = this._core.store.activeText;
|
|
20844
20994
|
if (activeText === null && object instanceof KritzelText) {
|
|
20845
20995
|
object.edit(event);
|
|
@@ -22514,6 +22664,7 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22514
22664
|
const existingGroup = this._core.store.selectionGroup;
|
|
22515
22665
|
if (shiftKey && existingGroup) {
|
|
22516
22666
|
existingGroup.addOrRemove(objectToSelect);
|
|
22667
|
+
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
22517
22668
|
if (existingGroup.objects.length === 0) {
|
|
22518
22669
|
this._core.removeSelectionGroup();
|
|
22519
22670
|
}
|
|
@@ -22559,15 +22710,17 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22559
22710
|
obj.isSelected = false;
|
|
22560
22711
|
}
|
|
22561
22712
|
const unrolledObjects = Array.from(resolvedObjects.values());
|
|
22713
|
+
const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
|
|
22562
22714
|
const existingGroup = this._core.store.selectionGroup;
|
|
22563
22715
|
if (shiftKey && existingGroup) {
|
|
22564
|
-
existingGroup.addObjects(
|
|
22716
|
+
existingGroup.addObjects(filteredObjects);
|
|
22717
|
+
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
22565
22718
|
this._core.rerender();
|
|
22566
22719
|
}
|
|
22567
22720
|
else {
|
|
22568
22721
|
// Create selection group and set all IDs at once (no per-item refresh)
|
|
22569
22722
|
const selectionGroup = KritzelSelectionGroup.create(this._core);
|
|
22570
|
-
selectionGroup.objectIds =
|
|
22723
|
+
selectionGroup.objectIds = filteredObjects.map(o => o.id);
|
|
22571
22724
|
// Only refresh dimensions and capture snapshots ONCE at the end - O(n) instead of O(n²)
|
|
22572
22725
|
if (selectionGroup.length === 1) {
|
|
22573
22726
|
selectionGroup.rotation = selectionGroup.objects[0].rotation;
|
|
@@ -22579,6 +22732,37 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22579
22732
|
this._core.rerender();
|
|
22580
22733
|
}
|
|
22581
22734
|
}
|
|
22735
|
+
/**
|
|
22736
|
+
* Removes objects that are descendants of any KritzelGroup also present in the list.
|
|
22737
|
+
* Prevents an ancestor group and one of its descendants from coexisting in a selection,
|
|
22738
|
+
* which would otherwise cause the descendant to be transformed twice (once via the
|
|
22739
|
+
* ancestor group, and once directly).
|
|
22740
|
+
* @param objects - The candidate objects for the selection.
|
|
22741
|
+
* @returns A new array with descendants of selected groups removed.
|
|
22742
|
+
*/
|
|
22743
|
+
filterDescendantsOfSelectedGroups(objects) {
|
|
22744
|
+
const descendantIds = new Set();
|
|
22745
|
+
for (const obj of objects) {
|
|
22746
|
+
if (obj instanceof KritzelGroup) {
|
|
22747
|
+
KritzelGroup.collectDescendantIds(obj, descendantIds);
|
|
22748
|
+
}
|
|
22749
|
+
}
|
|
22750
|
+
if (descendantIds.size === 0) {
|
|
22751
|
+
return objects;
|
|
22752
|
+
}
|
|
22753
|
+
return objects.filter(o => !descendantIds.has(o.id));
|
|
22754
|
+
}
|
|
22755
|
+
/**
|
|
22756
|
+
* Mutates an existing selection group to remove any descendants of groups
|
|
22757
|
+
* already present in it. See {@link filterDescendantsOfSelectedGroups}.
|
|
22758
|
+
* @param selectionGroup - The selection group to clean up in place.
|
|
22759
|
+
*/
|
|
22760
|
+
removeDescendantsOfSelectedGroups(selectionGroup) {
|
|
22761
|
+
const filtered = this.filterDescendantsOfSelectedGroups(selectionGroup.objects);
|
|
22762
|
+
if (filtered.length !== selectionGroup.objects.length) {
|
|
22763
|
+
selectionGroup.objectIds = filtered.map(o => o.id);
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22582
22766
|
}
|
|
22583
22767
|
|
|
22584
22768
|
/**
|
|
@@ -710,6 +710,31 @@ export class KritzelCore {
|
|
|
710
710
|
// Batch all inserts and updates in a single Y.js transaction to avoid
|
|
711
711
|
// N separate observer callbacks and N rerenders (fires only once at commit)
|
|
712
712
|
this._store.objects.transaction(() => {
|
|
713
|
+
// Recursively flush a group's pending children (and any nested groups'
|
|
714
|
+
// pending children) into the store. Without recursion, nested groups
|
|
715
|
+
// would have their grandchildren stranded in `_pendingChildren` and they
|
|
716
|
+
// would never appear on the canvas, even though the parent group's
|
|
717
|
+
// bounding box looks correct.
|
|
718
|
+
const flushPendingChildren = (group, parentZIndex) => {
|
|
719
|
+
if (group._pendingChildren.length === 0) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const pendingChildren = group._pendingChildren;
|
|
723
|
+
group._pendingChildren = [];
|
|
724
|
+
pendingChildren.forEach((child, childIndex) => {
|
|
725
|
+
if (child.workspaceId !== activeWorkspace.id) {
|
|
726
|
+
child.workspaceId = activeWorkspace.id;
|
|
727
|
+
}
|
|
728
|
+
child.updatePosition(child.translateX + offsetX, child.translateY + offsetY);
|
|
729
|
+
child.zIndex = parentZIndex + childIndex + 1;
|
|
730
|
+
// Recurse before adding so grandchildren are inserted as well
|
|
731
|
+
if (KritzelClassHelper.isInstanceOf(child, 'KritzelGroup')) {
|
|
732
|
+
flushPendingChildren(child, child.zIndex);
|
|
733
|
+
}
|
|
734
|
+
this.addObject(child);
|
|
735
|
+
});
|
|
736
|
+
group.finalize();
|
|
737
|
+
};
|
|
713
738
|
// First add all copied objects to the objectsMap with updated positions
|
|
714
739
|
copiedObjects.forEach((obj, i) => {
|
|
715
740
|
// Update workspace if pasting to a different workspace
|
|
@@ -720,26 +745,9 @@ export class KritzelCore {
|
|
|
720
745
|
obj.updatePosition(obj.translateX + offsetX, obj.translateY + offsetY);
|
|
721
746
|
// Update z-index
|
|
722
747
|
obj.zIndex = baseZIndex + i;
|
|
723
|
-
// Handle KritzelGroup: also add pending children with offset
|
|
748
|
+
// Handle KritzelGroup: also add pending children (and grandchildren) with offset
|
|
724
749
|
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
725
|
-
|
|
726
|
-
obj._pendingChildren.forEach((child, childIndex) => {
|
|
727
|
-
// Update workspace
|
|
728
|
-
if (child.workspaceId !== activeWorkspace.id) {
|
|
729
|
-
child.workspaceId = activeWorkspace.id;
|
|
730
|
-
}
|
|
731
|
-
// Update position with offset
|
|
732
|
-
child.updatePosition(child.translateX + offsetX, child.translateY + offsetY);
|
|
733
|
-
// Update z-index
|
|
734
|
-
child.zIndex = baseZIndex + i + childIndex;
|
|
735
|
-
// Add child to store
|
|
736
|
-
this.addObject(child);
|
|
737
|
-
});
|
|
738
|
-
// Clear pending children
|
|
739
|
-
obj._pendingChildren = [];
|
|
740
|
-
// Finalize the group's bounding box and snapshots
|
|
741
|
-
obj.finalize();
|
|
742
|
-
}
|
|
750
|
+
flushPendingChildren(obj, obj.zIndex);
|
|
743
751
|
}
|
|
744
752
|
// Add to objectsMap
|
|
745
753
|
this.addObject(obj);
|
|
@@ -505,6 +505,7 @@ export class KritzelViewport {
|
|
|
505
505
|
const startTranslateY = this._core.store.state.translateY;
|
|
506
506
|
const startScale = this._core.store.state.scale;
|
|
507
507
|
const startTime = performance.now();
|
|
508
|
+
this._core.store.state.isScaling = true;
|
|
508
509
|
const animate = (currentTime) => {
|
|
509
510
|
const elapsed = currentTime - startTime;
|
|
510
511
|
const progress = Math.min(elapsed / duration, 1);
|
|
@@ -523,6 +524,8 @@ export class KritzelViewport {
|
|
|
523
524
|
}
|
|
524
525
|
else {
|
|
525
526
|
this._animationFrameId = null;
|
|
527
|
+
this._core.store.state.isScaling = false;
|
|
528
|
+
this._core.rerender();
|
|
526
529
|
this._debounceUpdate();
|
|
527
530
|
}
|
|
528
531
|
};
|
|
@@ -14,7 +14,23 @@ export class KritzelKeyHandler extends KritzelBaseHandler {
|
|
|
14
14
|
*/
|
|
15
15
|
shortcuts = [
|
|
16
16
|
// General
|
|
17
|
-
{
|
|
17
|
+
{
|
|
18
|
+
key: 'Escape',
|
|
19
|
+
label: 'Clear Selection',
|
|
20
|
+
category: 'General',
|
|
21
|
+
condition: c => !!c.store.activeText || !!c.store.activeShape || !!c.store.selectionGroup,
|
|
22
|
+
action: c => {
|
|
23
|
+
if (c.store.activeText) {
|
|
24
|
+
c.store.activeText.handleEscape();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (c.store.activeShape) {
|
|
28
|
+
c.store.activeShape.handleEscape();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
c.clearSelection();
|
|
32
|
+
},
|
|
33
|
+
},
|
|
18
34
|
{ key: 'Delete', label: 'Delete Selected', category: 'General', condition: c => !!c.store.selectionGroup, action: c => c.delete() },
|
|
19
35
|
{ key: 'a', ctrl: true, label: 'Select All in Viewport', category: 'General', action: c => c.selectAllObjectsInViewport() },
|
|
20
36
|
{ key: 'A', ctrl: true, shift: true, label: 'Select All Objects', category: 'General', action: c => c.selectAllObjects() },
|
|
@@ -44,7 +60,14 @@ export class KritzelKeyHandler extends KritzelBaseHandler {
|
|
|
44
60
|
},
|
|
45
61
|
},
|
|
46
62
|
{ key: 'x', ctrl: true, label: 'Cut', category: 'Clipboard', condition: c => !!c.store.selectionGroup, action: c => c.cut() },
|
|
47
|
-
{
|
|
63
|
+
{
|
|
64
|
+
key: 'v',
|
|
65
|
+
ctrl: true,
|
|
66
|
+
label: 'Paste',
|
|
67
|
+
category: 'Clipboard',
|
|
68
|
+
condition: c => !!c.store.state.copiedObjects && !c.store.activeText && !c.store.activeShape,
|
|
69
|
+
action: c => c.paste(),
|
|
70
|
+
},
|
|
48
71
|
// Object layering
|
|
49
72
|
{ key: '+', ctrl: true, label: 'Bring Forward', category: 'Object Layering', condition: c => !!c.store.selectionGroup, action: c => c.bringForward() },
|
|
50
73
|
{ key: '-', ctrl: true, label: 'Send Backward', category: 'Object Layering', condition: c => !!c.store.selectionGroup, action: c => c.sendBackward() },
|
|
@@ -54,10 +77,46 @@ export class KritzelKeyHandler extends KritzelBaseHandler {
|
|
|
54
77
|
{ key: 'g', ctrl: true, label: 'Group', category: 'Grouping', condition: c => !!c.store.selectionGroup && c.store.selectionGroup.objects.length >= 2, action: c => c.group() },
|
|
55
78
|
{ key: 'G', ctrl: true, shift: true, label: 'Ungroup', category: 'Grouping', condition: c => !!c.store.selectionGroup, action: c => c.ungroup() },
|
|
56
79
|
// Movement
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
{
|
|
81
|
+
key: 'ArrowUp',
|
|
82
|
+
label: 'Move Object Up',
|
|
83
|
+
category: 'Movement',
|
|
84
|
+
condition: c => !!c.store.selectionGroup,
|
|
85
|
+
action: c => {
|
|
86
|
+
c.store.selectionGroup.move(0, 0, 0, NUDGE_AMOUNT);
|
|
87
|
+
c.rerender();
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'ArrowDown',
|
|
92
|
+
label: 'Move Object Down',
|
|
93
|
+
category: 'Movement',
|
|
94
|
+
condition: c => !!c.store.selectionGroup,
|
|
95
|
+
action: c => {
|
|
96
|
+
c.store.selectionGroup.move(0, NUDGE_AMOUNT, 0, 0);
|
|
97
|
+
c.rerender();
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'ArrowLeft',
|
|
102
|
+
label: 'Move Object Left',
|
|
103
|
+
category: 'Movement',
|
|
104
|
+
condition: c => !!c.store.selectionGroup,
|
|
105
|
+
action: c => {
|
|
106
|
+
c.store.selectionGroup.move(0, 0, NUDGE_AMOUNT, 0);
|
|
107
|
+
c.rerender();
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: 'ArrowRight',
|
|
112
|
+
label: 'Move Object Right',
|
|
113
|
+
category: 'Movement',
|
|
114
|
+
condition: c => !!c.store.selectionGroup,
|
|
115
|
+
action: c => {
|
|
116
|
+
c.store.selectionGroup.move(NUDGE_AMOUNT, 0, 0, 0);
|
|
117
|
+
c.rerender();
|
|
118
|
+
},
|
|
119
|
+
},
|
|
61
120
|
];
|
|
62
121
|
/**
|
|
63
122
|
* Creates a new instance of the key handler.
|
|
@@ -87,6 +146,14 @@ export class KritzelKeyHandler extends KritzelBaseHandler {
|
|
|
87
146
|
*/
|
|
88
147
|
handleKeyDown(event) {
|
|
89
148
|
this._core.store.state.isCtrlKeyPressed = event.ctrlKey;
|
|
149
|
+
// While a text or shape's text editor is being edited, let ProseMirror /
|
|
150
|
+
// the browser handle keys natively (Ctrl+A, Ctrl+C, arrows, etc.) instead
|
|
151
|
+
// of running global canvas shortcuts. Escape is the one exception — it
|
|
152
|
+
// still runs through the shortcut system so it can collapse the text
|
|
153
|
+
// selection (handled inside the shortcut's action).
|
|
154
|
+
if ((this._core.store.activeText || this._core.store.activeShape) && event.key !== 'Escape') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
90
157
|
const shortcut = this.shortcuts.find(s => s.key === event.key && !!s.ctrl === event.ctrlKey && !!s.shift === event.shiftKey && (!s.condition || s.condition(this._core)));
|
|
91
158
|
if (shortcut) {
|
|
92
159
|
event.preventDefault();
|
|
@@ -407,6 +407,7 @@ export class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
407
407
|
const existingGroup = this._core.store.selectionGroup;
|
|
408
408
|
if (shiftKey && existingGroup) {
|
|
409
409
|
existingGroup.addOrRemove(objectToSelect);
|
|
410
|
+
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
410
411
|
if (existingGroup.objects.length === 0) {
|
|
411
412
|
this._core.removeSelectionGroup();
|
|
412
413
|
}
|
|
@@ -452,15 +453,17 @@ export class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
452
453
|
obj.isSelected = false;
|
|
453
454
|
}
|
|
454
455
|
const unrolledObjects = Array.from(resolvedObjects.values());
|
|
456
|
+
const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
|
|
455
457
|
const existingGroup = this._core.store.selectionGroup;
|
|
456
458
|
if (shiftKey && existingGroup) {
|
|
457
|
-
existingGroup.addObjects(
|
|
459
|
+
existingGroup.addObjects(filteredObjects);
|
|
460
|
+
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
458
461
|
this._core.rerender();
|
|
459
462
|
}
|
|
460
463
|
else {
|
|
461
464
|
// Create selection group and set all IDs at once (no per-item refresh)
|
|
462
465
|
const selectionGroup = KritzelSelectionGroup.create(this._core);
|
|
463
|
-
selectionGroup.objectIds =
|
|
466
|
+
selectionGroup.objectIds = filteredObjects.map(o => o.id);
|
|
464
467
|
// Only refresh dimensions and capture snapshots ONCE at the end - O(n) instead of O(n²)
|
|
465
468
|
if (selectionGroup.length === 1) {
|
|
466
469
|
selectionGroup.rotation = selectionGroup.objects[0].rotation;
|
|
@@ -472,4 +475,35 @@ export class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
472
475
|
this._core.rerender();
|
|
473
476
|
}
|
|
474
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Removes objects that are descendants of any KritzelGroup also present in the list.
|
|
480
|
+
* Prevents an ancestor group and one of its descendants from coexisting in a selection,
|
|
481
|
+
* which would otherwise cause the descendant to be transformed twice (once via the
|
|
482
|
+
* ancestor group, and once directly).
|
|
483
|
+
* @param objects - The candidate objects for the selection.
|
|
484
|
+
* @returns A new array with descendants of selected groups removed.
|
|
485
|
+
*/
|
|
486
|
+
filterDescendantsOfSelectedGroups(objects) {
|
|
487
|
+
const descendantIds = new Set();
|
|
488
|
+
for (const obj of objects) {
|
|
489
|
+
if (obj instanceof KritzelGroup) {
|
|
490
|
+
KritzelGroup.collectDescendantIds(obj, descendantIds);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (descendantIds.size === 0) {
|
|
494
|
+
return objects;
|
|
495
|
+
}
|
|
496
|
+
return objects.filter(o => !descendantIds.has(o.id));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Mutates an existing selection group to remove any descendants of groups
|
|
500
|
+
* already present in it. See {@link filterDescendantsOfSelectedGroups}.
|
|
501
|
+
* @param selectionGroup - The selection group to clean up in place.
|
|
502
|
+
*/
|
|
503
|
+
removeDescendantsOfSelectedGroups(selectionGroup) {
|
|
504
|
+
const filtered = this.filterDescendantsOfSelectedGroups(selectionGroup.objects);
|
|
505
|
+
if (filtered.length !== selectionGroup.objects.length) {
|
|
506
|
+
selectionGroup.objectIds = filtered.map(o => o.id);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
475
509
|
}
|