@bensitu/image-editor 1.4.0 → 1.4.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/image-editor.esm.js +551 -185
- package/dist/image-editor.esm.js.map +3 -3
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +551 -185
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +551 -185
- package/dist/image-editor.js.map +3 -3
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +24 -12
- package/package.json +3 -4
- package/src/image-editor.js +608 -188
package/dist/image-editor.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file image-editor.js
|
|
5
5
|
* @module image-editor
|
|
6
|
-
* @version 1.4.
|
|
6
|
+
* @version 1.4.2
|
|
7
7
|
* @author Ben Situ
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
10
10
|
*/
|
|
11
11
|
var fabric = null;
|
|
12
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol.for("ImageEditorInternalOperation");
|
|
12
13
|
function getGlobalScope() {
|
|
13
14
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
14
15
|
if (typeof self !== "undefined") return self;
|
|
@@ -116,11 +117,15 @@
|
|
|
116
117
|
this.currentRotation = 0;
|
|
117
118
|
this.maskCounter = 0;
|
|
118
119
|
this.isAnimating = false;
|
|
120
|
+
this._isLoading = false;
|
|
121
|
+
this._activeOperationName = null;
|
|
122
|
+
this._activeOperationToken = null;
|
|
119
123
|
this.elements = {};
|
|
120
124
|
this.isImageLoadedToCanvas = false;
|
|
121
125
|
this.maxHistorySize = 50;
|
|
122
126
|
this._handlersByElementKey = {};
|
|
123
127
|
this._elementCache = {};
|
|
128
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
124
129
|
this._lastMask = null;
|
|
125
130
|
this._lastMaskInitialLeft = null;
|
|
126
131
|
this._lastMaskInitialTop = null;
|
|
@@ -209,6 +214,10 @@
|
|
|
209
214
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
210
215
|
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
211
216
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
217
|
+
this._isLoading = false;
|
|
218
|
+
this._activeOperationName = null;
|
|
219
|
+
this._activeOperationToken = null;
|
|
220
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
212
221
|
this._containerOriginalOverflow = null;
|
|
213
222
|
this._lastContainerViewportSize = null;
|
|
214
223
|
this._canvasElementOriginalStyle = null;
|
|
@@ -415,6 +424,12 @@
|
|
|
415
424
|
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
416
425
|
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
417
426
|
}
|
|
427
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
428
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
429
|
+
this.containerElement.style.overflow = snapshot.overflow || "";
|
|
430
|
+
this.containerElement.style.overflowX = snapshot.overflowX || "";
|
|
431
|
+
this.containerElement.style.overflowY = snapshot.overflowY || "";
|
|
432
|
+
}
|
|
418
433
|
/**
|
|
419
434
|
* DOM / UI bindings
|
|
420
435
|
* @private
|
|
@@ -549,19 +564,23 @@
|
|
|
549
564
|
if (!this._fabricLoaded) return;
|
|
550
565
|
if (!this.canvas || this._disposed) return;
|
|
551
566
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
552
|
-
this._assertIdleForOperation("loadImage");
|
|
567
|
+
this._assertIdleForOperation("loadImage", options);
|
|
568
|
+
this._isLoading = true;
|
|
569
|
+
this._updateUI();
|
|
553
570
|
this._warnOnImageLayoutOptionConflict();
|
|
554
571
|
const transaction = this._captureLoadImageTransaction();
|
|
555
572
|
try {
|
|
556
573
|
const imageElement = await this._createImageElement(imageBase64);
|
|
557
574
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
558
575
|
let loadSource = imageBase64;
|
|
559
|
-
|
|
560
|
-
|
|
576
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
577
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
578
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
579
|
+
const shouldResize = imageElement.naturalWidth > downsampleMaxWidth || imageElement.naturalHeight > downsampleMaxHeight;
|
|
561
580
|
if (shouldResize) {
|
|
562
581
|
const ratio = Math.min(
|
|
563
|
-
|
|
564
|
-
|
|
582
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
583
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
565
584
|
);
|
|
566
585
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
567
586
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -569,10 +588,12 @@
|
|
|
569
588
|
imageElement,
|
|
570
589
|
targetWidth,
|
|
571
590
|
targetHeight,
|
|
572
|
-
this.options.downsampleQuality,
|
|
591
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
573
592
|
imageBase64
|
|
574
593
|
);
|
|
575
594
|
}
|
|
595
|
+
} else if (this.options.downsampleOnLoad) {
|
|
596
|
+
this._reportWarning("loadImage: downsample limits must be positive numbers; using the original image");
|
|
576
597
|
}
|
|
577
598
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
578
599
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
@@ -637,6 +658,9 @@
|
|
|
637
658
|
} catch (error) {
|
|
638
659
|
await this._rollbackLoadImageTransaction(transaction);
|
|
639
660
|
throw error;
|
|
661
|
+
} finally {
|
|
662
|
+
this._isLoading = false;
|
|
663
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
640
664
|
}
|
|
641
665
|
}
|
|
642
666
|
/**
|
|
@@ -647,6 +671,15 @@
|
|
|
647
671
|
const fabricInstance = ensureFabric();
|
|
648
672
|
return !!(this.originalImage && fabricInstance && this.originalImage instanceof fabricInstance.Image && this.originalImage.width > 0 && this.originalImage.height > 0);
|
|
649
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
676
|
+
*
|
|
677
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
678
|
+
* @public
|
|
679
|
+
*/
|
|
680
|
+
isBusy() {
|
|
681
|
+
return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
682
|
+
}
|
|
650
683
|
/**
|
|
651
684
|
* Creates an HTMLImageElement from a given data URL.
|
|
652
685
|
*
|
|
@@ -718,7 +751,6 @@
|
|
|
718
751
|
_captureLoadImageTransaction() {
|
|
719
752
|
return {
|
|
720
753
|
canvasState: this._serializeCanvasState(),
|
|
721
|
-
originalImage: this.originalImage,
|
|
722
754
|
baseImageScale: this.baseImageScale,
|
|
723
755
|
currentScale: this.currentScale,
|
|
724
756
|
currentRotation: this.currentRotation,
|
|
@@ -742,9 +774,14 @@
|
|
|
742
774
|
}
|
|
743
775
|
async _rollbackLoadImageTransaction(transaction) {
|
|
744
776
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
777
|
+
let didRestoreCanvasState = false;
|
|
745
778
|
try {
|
|
746
|
-
if (transaction.canvasState)
|
|
779
|
+
if (transaction.canvasState) {
|
|
780
|
+
await this.loadFromState(transaction.canvasState);
|
|
781
|
+
didRestoreCanvasState = true;
|
|
782
|
+
}
|
|
747
783
|
} catch (error) {
|
|
784
|
+
this._lastMask = null;
|
|
748
785
|
this._reportError("loadImage rollback failed", error);
|
|
749
786
|
}
|
|
750
787
|
this.baseImageScale = transaction.baseImageScale;
|
|
@@ -753,22 +790,40 @@
|
|
|
753
790
|
this.maskCounter = transaction.maskCounter;
|
|
754
791
|
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
755
792
|
this._lastSnapshot = transaction.lastSnapshot;
|
|
793
|
+
if (didRestoreCanvasState) {
|
|
794
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
795
|
+
} else {
|
|
796
|
+
this._lastMask = null;
|
|
797
|
+
}
|
|
756
798
|
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
757
799
|
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
758
800
|
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
759
|
-
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
760
801
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
761
802
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
762
803
|
if (this.containerElement) {
|
|
763
804
|
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
764
805
|
this.containerElement.scrollTop = transaction.scrollTop;
|
|
765
|
-
this.
|
|
806
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
766
807
|
}
|
|
767
808
|
this._updateInputs();
|
|
768
809
|
this._updateMaskList();
|
|
769
810
|
this._updateUI();
|
|
770
811
|
if (this.canvas) this.canvas.renderAll();
|
|
771
812
|
}
|
|
813
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
814
|
+
if (!this.canvas) {
|
|
815
|
+
this._lastMask = null;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
819
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
820
|
+
this._lastMask = masks.find((mask) => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
821
|
+
if (!this._lastMask) {
|
|
822
|
+
this._lastMaskInitialLeft = null;
|
|
823
|
+
this._lastMaskInitialTop = null;
|
|
824
|
+
this._lastMaskInitialWidth = null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
772
827
|
/**
|
|
773
828
|
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
774
829
|
*
|
|
@@ -781,12 +836,19 @@
|
|
|
781
836
|
* @private
|
|
782
837
|
*/
|
|
783
838
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
839
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
840
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
841
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
842
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
843
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
844
|
+
throw new Error("Invalid image resample target dimensions");
|
|
845
|
+
}
|
|
784
846
|
const offscreenCanvas = document.createElement("canvas");
|
|
785
|
-
offscreenCanvas.width =
|
|
786
|
-
offscreenCanvas.height =
|
|
847
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
848
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
787
849
|
const context = offscreenCanvas.getContext("2d");
|
|
788
850
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
789
|
-
context.drawImage(imageElement, 0, 0,
|
|
851
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
790
852
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
791
853
|
}
|
|
792
854
|
_getDataUrlMimeType(dataUrl) {
|
|
@@ -1052,7 +1114,11 @@
|
|
|
1052
1114
|
maskStyleBackups.push(backup);
|
|
1053
1115
|
mask.set(stylePatch);
|
|
1054
1116
|
});
|
|
1055
|
-
|
|
1117
|
+
const result = callback();
|
|
1118
|
+
if (result && typeof result.then === "function") {
|
|
1119
|
+
throw new Error("_withNormalizedMaskStyles callback must be synchronous");
|
|
1120
|
+
}
|
|
1121
|
+
return result;
|
|
1056
1122
|
} finally {
|
|
1057
1123
|
maskStyleBackups.forEach((backup) => {
|
|
1058
1124
|
try {
|
|
@@ -1120,9 +1186,13 @@
|
|
|
1120
1186
|
* @returns {number} A finite quality value between 0 and 1.
|
|
1121
1187
|
* @private
|
|
1122
1188
|
*/
|
|
1123
|
-
_normalizeQuality(quality) {
|
|
1189
|
+
_normalizeQuality(quality, fallback = void 0) {
|
|
1190
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1191
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1192
|
+
const safeFallback = Number.isFinite(numericFallback) ? Math.max(0, Math.min(1, numericFallback)) : 0.92;
|
|
1193
|
+
if (quality == null) return safeFallback;
|
|
1124
1194
|
const numericQuality = Number(quality);
|
|
1125
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1195
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
1126
1196
|
return Math.max(0, Math.min(1, numericQuality));
|
|
1127
1197
|
}
|
|
1128
1198
|
/**
|
|
@@ -1173,64 +1243,63 @@
|
|
|
1173
1243
|
sourceHeight: Math.max(1, endY - sourceY)
|
|
1174
1244
|
};
|
|
1175
1245
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
});
|
|
1246
|
+
_hasFractionalCanvasEdge(value) {
|
|
1247
|
+
const numericValue = Number(value);
|
|
1248
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1249
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1250
|
+
}
|
|
1251
|
+
_getPartialExportEdges(bounds) {
|
|
1252
|
+
if (!bounds) return null;
|
|
1253
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1254
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1255
|
+
if (!isAxisAligned) return null;
|
|
1256
|
+
return {
|
|
1257
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1258
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1259
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1260
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1264
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1265
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1266
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1267
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1268
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1269
|
+
offscreenCanvas.width = width;
|
|
1270
|
+
offscreenCanvas.height = height;
|
|
1271
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1272
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1273
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1274
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1275
|
+
const pixels = imageData.data;
|
|
1276
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1277
|
+
const index = (y * width + x) * 4;
|
|
1278
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1279
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1280
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1281
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1282
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1283
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1284
|
+
}
|
|
1285
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1286
|
+
pixels[index + 3] = 255;
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
if (edges.left && width > 1) {
|
|
1290
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1291
|
+
}
|
|
1292
|
+
if (edges.right && width > 1) {
|
|
1293
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1294
|
+
}
|
|
1295
|
+
if (edges.top && height > 1) {
|
|
1296
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1297
|
+
}
|
|
1298
|
+
if (edges.bottom && height > 1) {
|
|
1299
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1300
|
+
}
|
|
1301
|
+
context.putImageData(imageData, 0, 0);
|
|
1302
|
+
return offscreenCanvas.toDataURL("image/png");
|
|
1234
1303
|
}
|
|
1235
1304
|
/**
|
|
1236
1305
|
* Exports a source region directly through Fabric's region export options.
|
|
@@ -1243,13 +1312,16 @@
|
|
|
1243
1312
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1244
1313
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1245
1314
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1315
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1246
1316
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1247
1317
|
* @private
|
|
1248
1318
|
*/
|
|
1249
|
-
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1319
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1250
1320
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1251
|
-
|
|
1252
|
-
|
|
1321
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1322
|
+
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1323
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1324
|
+
format: exportFormat,
|
|
1253
1325
|
quality,
|
|
1254
1326
|
multiplier: safeMultiplier,
|
|
1255
1327
|
left: sourceX,
|
|
@@ -1257,6 +1329,61 @@
|
|
|
1257
1329
|
width: sourceWidth,
|
|
1258
1330
|
height: sourceHeight
|
|
1259
1331
|
});
|
|
1332
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1333
|
+
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1334
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1335
|
+
}
|
|
1336
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1337
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1338
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1339
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1340
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1341
|
+
offscreenCanvas.width = width;
|
|
1342
|
+
offscreenCanvas.height = height;
|
|
1343
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1344
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1345
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1346
|
+
context.fillRect(0, 0, width, height);
|
|
1347
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1348
|
+
return offscreenCanvas.toDataURL("image/jpeg", this._normalizeQuality(quality));
|
|
1349
|
+
}
|
|
1350
|
+
_getJpegBackgroundColor() {
|
|
1351
|
+
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1352
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1353
|
+
return backgroundColor;
|
|
1354
|
+
}
|
|
1355
|
+
_isTransparentCssColor(color) {
|
|
1356
|
+
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
1357
|
+
if (!normalizedColor || normalizedColor === "transparent") return true;
|
|
1358
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1359
|
+
if (hexAlphaMatch) {
|
|
1360
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1361
|
+
return alpha === "0" || alpha === "00";
|
|
1362
|
+
}
|
|
1363
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1364
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1365
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1366
|
+
if (commaAlphaMatch) {
|
|
1367
|
+
const parts = commaAlphaMatch[1].split(",");
|
|
1368
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1373
|
+
const normalizedAlpha = String(alphaValue || "").trim();
|
|
1374
|
+
if (!normalizedAlpha) return false;
|
|
1375
|
+
if (normalizedAlpha.endsWith("%")) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1376
|
+
return Number(normalizedAlpha) === 0;
|
|
1377
|
+
}
|
|
1378
|
+
_decodeBase64Payload(base64Payload) {
|
|
1379
|
+
const payload = String(base64Payload || "");
|
|
1380
|
+
if (typeof atob === "function") {
|
|
1381
|
+
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
|
|
1384
|
+
return new Uint8Array(Buffer.from(payload, "base64"));
|
|
1385
|
+
}
|
|
1386
|
+
throw new Error("Base64 decoding is unavailable");
|
|
1260
1387
|
}
|
|
1261
1388
|
/**
|
|
1262
1389
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1414,17 +1541,70 @@
|
|
|
1414
1541
|
* @public
|
|
1415
1542
|
*/
|
|
1416
1543
|
scaleImage(factor, options = {}) {
|
|
1417
|
-
|
|
1544
|
+
try {
|
|
1545
|
+
this._assertCanQueueAnimation("scaleImage", options);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
return Promise.reject(error);
|
|
1548
|
+
}
|
|
1549
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options)).finally(() => {
|
|
1550
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
_getInternalOperationToken(options) {
|
|
1554
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1555
|
+
}
|
|
1556
|
+
_isOwnInternalOperation(options) {
|
|
1557
|
+
const token = this._getInternalOperationToken(options);
|
|
1558
|
+
return !!token && token === this._activeOperationToken;
|
|
1418
1559
|
}
|
|
1419
|
-
|
|
1560
|
+
_beginBusyOperation(operationName) {
|
|
1561
|
+
const token = Symbol(operationName);
|
|
1562
|
+
this._activeOperationName = operationName;
|
|
1563
|
+
this._activeOperationToken = token;
|
|
1564
|
+
this._updateUI();
|
|
1565
|
+
return token;
|
|
1566
|
+
}
|
|
1567
|
+
_endBusyOperation(token) {
|
|
1568
|
+
if (token && token === this._activeOperationToken) {
|
|
1569
|
+
this._activeOperationName = null;
|
|
1570
|
+
this._activeOperationToken = null;
|
|
1571
|
+
this._updateUI();
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1575
|
+
return {
|
|
1576
|
+
...options,
|
|
1577
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
_assertEditorAvailable(operationName) {
|
|
1420
1581
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1582
|
+
}
|
|
1583
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1584
|
+
this._assertEditorAvailable(operationName);
|
|
1585
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1421
1586
|
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1422
1587
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1423
1588
|
}
|
|
1589
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1590
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1591
|
+
}
|
|
1592
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1593
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1597
|
+
this._assertEditorAvailable(operationName);
|
|
1598
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1599
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1600
|
+
}
|
|
1601
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1602
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1603
|
+
}
|
|
1424
1604
|
}
|
|
1425
|
-
_canMutateNow(operationName) {
|
|
1605
|
+
_canMutateNow(operationName, options = {}) {
|
|
1426
1606
|
try {
|
|
1427
|
-
this._assertIdleForOperation(operationName);
|
|
1607
|
+
this._assertIdleForOperation(operationName, options);
|
|
1428
1608
|
return true;
|
|
1429
1609
|
} catch (error) {
|
|
1430
1610
|
this._reportError(`${operationName} blocked`, error);
|
|
@@ -1529,7 +1709,14 @@
|
|
|
1529
1709
|
* @public
|
|
1530
1710
|
*/
|
|
1531
1711
|
rotateImage(degrees, options = {}) {
|
|
1532
|
-
|
|
1712
|
+
try {
|
|
1713
|
+
this._assertCanQueueAnimation("rotateImage", options);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
return Promise.reject(error);
|
|
1716
|
+
}
|
|
1717
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options)).finally(() => {
|
|
1718
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1719
|
+
});
|
|
1533
1720
|
}
|
|
1534
1721
|
/**
|
|
1535
1722
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1591,12 +1778,19 @@
|
|
|
1591
1778
|
*/
|
|
1592
1779
|
resetImageTransform() {
|
|
1593
1780
|
if (!this.originalImage) return Promise.resolve();
|
|
1781
|
+
try {
|
|
1782
|
+
this._assertCanQueueAnimation("resetImageTransform");
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
return Promise.reject(error);
|
|
1785
|
+
}
|
|
1594
1786
|
return this.animationQueue.add(async () => {
|
|
1595
1787
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1596
1788
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1597
1789
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1598
1790
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1599
1791
|
this._pushStateTransition(before, after);
|
|
1792
|
+
}).finally(() => {
|
|
1793
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1600
1794
|
}).catch((error) => {
|
|
1601
1795
|
this._reportError("resetImageTransform() failed", error);
|
|
1602
1796
|
throw error;
|
|
@@ -1633,6 +1827,9 @@
|
|
|
1633
1827
|
try {
|
|
1634
1828
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1635
1829
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1830
|
+
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1831
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1832
|
+
}
|
|
1636
1833
|
this.canvas.loadFromJSON(state, async () => {
|
|
1637
1834
|
try {
|
|
1638
1835
|
if (this._disposed || !this.canvas) {
|
|
@@ -1711,22 +1908,46 @@
|
|
|
1711
1908
|
}
|
|
1712
1909
|
_waitForImageElementReady(imageElement) {
|
|
1713
1910
|
if (!imageElement) return Promise.resolve();
|
|
1714
|
-
|
|
1911
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1912
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
1913
|
+
if (imageElement.complete) return Promise.reject(new Error("Image could not be loaded while restoring state"));
|
|
1715
1914
|
return new Promise((resolve, reject) => {
|
|
1716
1915
|
let isSettled = false;
|
|
1717
|
-
|
|
1718
|
-
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1719
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1916
|
+
let timerId;
|
|
1720
1917
|
const settle = (callback) => {
|
|
1721
1918
|
if (isSettled) return;
|
|
1722
1919
|
isSettled = true;
|
|
1723
1920
|
clearTimeout(timerId);
|
|
1724
|
-
imageElement.
|
|
1725
|
-
|
|
1921
|
+
if (typeof imageElement.removeEventListener === "function") {
|
|
1922
|
+
imageElement.removeEventListener("load", handleLoad);
|
|
1923
|
+
imageElement.removeEventListener("error", handleError);
|
|
1924
|
+
} else {
|
|
1925
|
+
imageElement.onload = null;
|
|
1926
|
+
imageElement.onerror = null;
|
|
1927
|
+
}
|
|
1726
1928
|
callback();
|
|
1727
1929
|
};
|
|
1728
|
-
|
|
1729
|
-
|
|
1930
|
+
const handleLoad = () => {
|
|
1931
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1932
|
+
settle(() => {
|
|
1933
|
+
if (didLoad) {
|
|
1934
|
+
resolve();
|
|
1935
|
+
} else {
|
|
1936
|
+
reject(new Error("Image could not be loaded while restoring state"));
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
};
|
|
1940
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error("Image could not be loaded while restoring state")));
|
|
1941
|
+
timerId = setTimeout(() => {
|
|
1942
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1943
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1944
|
+
if (typeof imageElement.addEventListener === "function") {
|
|
1945
|
+
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1946
|
+
imageElement.addEventListener("error", handleError, { once: true });
|
|
1947
|
+
} else {
|
|
1948
|
+
imageElement.onload = handleLoad;
|
|
1949
|
+
imageElement.onerror = handleError;
|
|
1950
|
+
}
|
|
1730
1951
|
});
|
|
1731
1952
|
}
|
|
1732
1953
|
/**
|
|
@@ -1991,6 +2212,10 @@
|
|
|
1991
2212
|
});
|
|
1992
2213
|
}
|
|
1993
2214
|
}
|
|
2215
|
+
if (!mask || typeof mask.set !== "function" || typeof mask.setCoords !== "function") {
|
|
2216
|
+
this._reportWarning("fabricGenerator returned an invalid Fabric object");
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
1994
2219
|
const styles = maskConfig.styles || {};
|
|
1995
2220
|
const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
|
|
1996
2221
|
const maskSettings = {
|
|
@@ -2079,7 +2304,7 @@
|
|
|
2079
2304
|
*/
|
|
2080
2305
|
removeAllMasks(options = {}) {
|
|
2081
2306
|
if (!this.canvas) return;
|
|
2082
|
-
if (!this._canMutateNow("removeAllMasks")) return;
|
|
2307
|
+
if (!this._canMutateNow("removeAllMasks", options)) return;
|
|
2083
2308
|
const saveHistory = options.saveHistory !== false;
|
|
2084
2309
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2085
2310
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2118,6 +2343,93 @@
|
|
|
2118
2343
|
}
|
|
2119
2344
|
}
|
|
2120
2345
|
}
|
|
2346
|
+
_captureMaskLabelBackups(masks) {
|
|
2347
|
+
if (!this.canvas) return [];
|
|
2348
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2349
|
+
return (masks || []).map((mask) => {
|
|
2350
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2351
|
+
return {
|
|
2352
|
+
mask,
|
|
2353
|
+
label,
|
|
2354
|
+
hadLabel: !!label,
|
|
2355
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2356
|
+
visible: label ? label.visible : void 0
|
|
2357
|
+
};
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2361
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2362
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2363
|
+
labelBackups.forEach((backup) => {
|
|
2364
|
+
if (!backup || !backup.mask) return;
|
|
2365
|
+
try {
|
|
2366
|
+
if (!backup.hadLabel) {
|
|
2367
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
backup.mask.__label = backup.label;
|
|
2371
|
+
if (!backup.label) return;
|
|
2372
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2373
|
+
this.canvas.add(backup.label);
|
|
2374
|
+
canvasObjects.add(backup.label);
|
|
2375
|
+
}
|
|
2376
|
+
if (backup.visible !== void 0) backup.label.set({ visible: backup.visible });
|
|
2377
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2378
|
+
this._syncMaskLabel(backup.mask);
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
void error;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
_captureActiveObjectBackup() {
|
|
2385
|
+
if (!this.canvas) return null;
|
|
2386
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2387
|
+
if (!activeObject) return null;
|
|
2388
|
+
const selectedObjects = typeof activeObject.getObjects === "function" ? activeObject.getObjects() : [activeObject];
|
|
2389
|
+
return { activeObject, selectedObjects };
|
|
2390
|
+
}
|
|
2391
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2392
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2393
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2394
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects) ? activeObjectBackup.selectedObjects : [];
|
|
2395
|
+
const canRestore = selectedObjects.length ? selectedObjects.every((object) => canvasObjects.includes(object)) : canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2396
|
+
if (!canRestore) return;
|
|
2397
|
+
try {
|
|
2398
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
void error;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
_captureMaskExportBackups(masks) {
|
|
2404
|
+
return (masks || []).map((mask) => ({
|
|
2405
|
+
object: mask,
|
|
2406
|
+
visible: mask.visible,
|
|
2407
|
+
opacity: mask.opacity,
|
|
2408
|
+
fill: mask.fill,
|
|
2409
|
+
strokeWidth: mask.strokeWidth,
|
|
2410
|
+
stroke: mask.stroke,
|
|
2411
|
+
selectable: mask.selectable,
|
|
2412
|
+
lockRotation: mask.lockRotation
|
|
2413
|
+
}));
|
|
2414
|
+
}
|
|
2415
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2416
|
+
(maskBackups || []).forEach((backup) => {
|
|
2417
|
+
try {
|
|
2418
|
+
backup.object.set({
|
|
2419
|
+
visible: backup.visible,
|
|
2420
|
+
opacity: backup.opacity,
|
|
2421
|
+
fill: backup.fill,
|
|
2422
|
+
strokeWidth: backup.strokeWidth,
|
|
2423
|
+
stroke: backup.stroke,
|
|
2424
|
+
selectable: backup.selectable,
|
|
2425
|
+
lockRotation: backup.lockRotation
|
|
2426
|
+
});
|
|
2427
|
+
backup.object.setCoords();
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
void error;
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2121
2433
|
/**
|
|
2122
2434
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2123
2435
|
*
|
|
@@ -2190,10 +2502,14 @@
|
|
|
2190
2502
|
_hideAllMaskLabels() {
|
|
2191
2503
|
if (!this.canvas) return;
|
|
2192
2504
|
const canvasObjects = this.canvas.getObjects();
|
|
2505
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2193
2506
|
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
2194
2507
|
labels.forEach((label) => {
|
|
2195
2508
|
try {
|
|
2196
|
-
if (
|
|
2509
|
+
if (canvasObjectSet.has(label)) {
|
|
2510
|
+
this.canvas.remove(label);
|
|
2511
|
+
canvasObjectSet.delete(label);
|
|
2512
|
+
}
|
|
2197
2513
|
} catch (error) {
|
|
2198
2514
|
void error;
|
|
2199
2515
|
}
|
|
@@ -2352,18 +2668,36 @@
|
|
|
2352
2668
|
this._assertIdleForOperation("mergeMasks");
|
|
2353
2669
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2354
2670
|
if (!masks.length) return;
|
|
2671
|
+
const beforeJson = this._serializeCanvasState();
|
|
2672
|
+
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2355
2673
|
this.canvas.discardActiveObject();
|
|
2356
2674
|
this.canvas.renderAll();
|
|
2357
2675
|
try {
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2676
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
2677
|
+
exportImageArea: true,
|
|
2678
|
+
multiplier: this.options.exportMultiplier,
|
|
2679
|
+
fileType: "png"
|
|
2680
|
+
}));
|
|
2681
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2682
|
+
if (this.canvas.getObjects().some((object) => object.maskId)) {
|
|
2683
|
+
throw new Error("Masks could not be removed during merge");
|
|
2684
|
+
}
|
|
2685
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2686
|
+
preserveScroll: true,
|
|
2687
|
+
resetMaskCounter: false
|
|
2688
|
+
}));
|
|
2362
2689
|
const afterJson = this._serializeCanvasState();
|
|
2363
2690
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2364
2691
|
} catch (error) {
|
|
2365
2692
|
this._reportError("merge error", error);
|
|
2693
|
+
try {
|
|
2694
|
+
await this.loadFromState(beforeJson);
|
|
2695
|
+
} catch (restoreError) {
|
|
2696
|
+
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2697
|
+
}
|
|
2366
2698
|
throw error;
|
|
2699
|
+
} finally {
|
|
2700
|
+
this._endBusyOperation(operationToken);
|
|
2367
2701
|
}
|
|
2368
2702
|
}
|
|
2369
2703
|
/**
|
|
@@ -2414,14 +2748,18 @@
|
|
|
2414
2748
|
*/
|
|
2415
2749
|
async exportImageBase64(options = {}) {
|
|
2416
2750
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2417
|
-
this._assertIdleForOperation("exportImageBase64");
|
|
2751
|
+
this._assertIdleForOperation("exportImageBase64", options);
|
|
2418
2752
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2419
2753
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2420
2754
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2421
2755
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2422
2756
|
if (!exportImageArea) {
|
|
2423
2757
|
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
2758
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2424
2759
|
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
2760
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
2761
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
2762
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
2425
2763
|
try {
|
|
2426
2764
|
masks2.forEach((mask) => {
|
|
2427
2765
|
mask.set({ visible: false });
|
|
@@ -2430,12 +2768,13 @@
|
|
|
2430
2768
|
this.canvas.renderAll();
|
|
2431
2769
|
this.originalImage.setCoords();
|
|
2432
2770
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2433
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2434
|
-
return this._exportCanvasRegionToDataURL({
|
|
2771
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2772
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2435
2773
|
...exportRegion,
|
|
2436
2774
|
multiplier,
|
|
2437
2775
|
quality,
|
|
2438
|
-
format
|
|
2776
|
+
format,
|
|
2777
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2439
2778
|
});
|
|
2440
2779
|
} finally {
|
|
2441
2780
|
maskVisibilityBackups.forEach((backup) => {
|
|
@@ -2445,19 +2784,16 @@
|
|
|
2445
2784
|
void error;
|
|
2446
2785
|
}
|
|
2447
2786
|
});
|
|
2787
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2788
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
2789
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
2448
2790
|
this.canvas.renderAll();
|
|
2449
2791
|
}
|
|
2450
2792
|
}
|
|
2451
2793
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2452
|
-
const maskStyleBackups =
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
fill: mask.fill,
|
|
2456
|
-
strokeWidth: mask.strokeWidth,
|
|
2457
|
-
stroke: mask.stroke,
|
|
2458
|
-
selectable: mask.selectable,
|
|
2459
|
-
lockRotation: mask.lockRotation
|
|
2460
|
-
}));
|
|
2794
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2795
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2796
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2461
2797
|
let finalBase64;
|
|
2462
2798
|
try {
|
|
2463
2799
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2470,29 +2806,18 @@
|
|
|
2470
2806
|
this.canvas.renderAll();
|
|
2471
2807
|
this.originalImage.setCoords();
|
|
2472
2808
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2473
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2474
|
-
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2809
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2810
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2475
2811
|
...exportRegion,
|
|
2476
2812
|
multiplier,
|
|
2477
2813
|
quality,
|
|
2478
|
-
format
|
|
2814
|
+
format,
|
|
2815
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2479
2816
|
});
|
|
2480
2817
|
} finally {
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
opacity: backup.opacity,
|
|
2485
|
-
fill: backup.fill,
|
|
2486
|
-
strokeWidth: backup.strokeWidth,
|
|
2487
|
-
stroke: backup.stroke,
|
|
2488
|
-
selectable: backup.selectable,
|
|
2489
|
-
lockRotation: backup.lockRotation
|
|
2490
|
-
});
|
|
2491
|
-
backup.object.setCoords();
|
|
2492
|
-
} catch (error) {
|
|
2493
|
-
void error;
|
|
2494
|
-
}
|
|
2495
|
-
});
|
|
2818
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
2819
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
2820
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2496
2821
|
this.canvas.renderAll();
|
|
2497
2822
|
}
|
|
2498
2823
|
return finalBase64;
|
|
@@ -2536,19 +2861,20 @@
|
|
|
2536
2861
|
fileName = this.options.defaultDownloadFileName ?? "exported_image.jpg"
|
|
2537
2862
|
} = options;
|
|
2538
2863
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2864
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2539
2865
|
let imageBase64;
|
|
2540
2866
|
if (mergeMask) {
|
|
2541
2867
|
imageBase64 = await this.exportImageBase64({
|
|
2542
2868
|
exportImageArea: true,
|
|
2543
2869
|
multiplier,
|
|
2544
|
-
quality,
|
|
2870
|
+
quality: normalizedQuality,
|
|
2545
2871
|
fileType: safeFileType
|
|
2546
2872
|
});
|
|
2547
2873
|
} else {
|
|
2548
2874
|
imageBase64 = await this.exportImageBase64({
|
|
2549
2875
|
exportImageArea: false,
|
|
2550
2876
|
multiplier,
|
|
2551
|
-
quality,
|
|
2877
|
+
quality: normalizedQuality,
|
|
2552
2878
|
fileType: safeFileType
|
|
2553
2879
|
});
|
|
2554
2880
|
}
|
|
@@ -2565,7 +2891,7 @@
|
|
|
2565
2891
|
const context = offscreenCanvas.getContext("2d");
|
|
2566
2892
|
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2567
2893
|
context.drawImage(imageElement, 0, 0);
|
|
2568
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
2894
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2569
2895
|
resolve(convertedDataUrl);
|
|
2570
2896
|
} catch (error) {
|
|
2571
2897
|
reject(error);
|
|
@@ -2575,13 +2901,8 @@
|
|
|
2575
2901
|
imageElement.src = imageBase64;
|
|
2576
2902
|
});
|
|
2577
2903
|
}
|
|
2578
|
-
const
|
|
2904
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2579
2905
|
const mime = `image/${safeFileType}`;
|
|
2580
|
-
let byteIndex = binaryString.length;
|
|
2581
|
-
const bytes = new Uint8Array(byteIndex);
|
|
2582
|
-
while (byteIndex--) {
|
|
2583
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
2584
|
-
}
|
|
2585
2906
|
return new File([bytes], fileName, { type: mime });
|
|
2586
2907
|
}
|
|
2587
2908
|
_clearMaskPlacementMemory() {
|
|
@@ -2728,6 +3049,30 @@
|
|
|
2728
3049
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
2729
3050
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
2730
3051
|
cropRect.setCoords();
|
|
3052
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3053
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3054
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3055
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3056
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3057
|
+
let deltaX = 0;
|
|
3058
|
+
let deltaY = 0;
|
|
3059
|
+
if (cropBounds.left < imageLeft) {
|
|
3060
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3061
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3062
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3063
|
+
}
|
|
3064
|
+
if (cropBounds.top < imageTop) {
|
|
3065
|
+
deltaY = imageTop - cropBounds.top;
|
|
3066
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3067
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3068
|
+
}
|
|
3069
|
+
if (deltaX || deltaY) {
|
|
3070
|
+
cropRect.set({
|
|
3071
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3072
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3073
|
+
});
|
|
3074
|
+
cropRect.setCoords();
|
|
3075
|
+
}
|
|
2731
3076
|
this.canvas.requestRenderAll();
|
|
2732
3077
|
} catch (error) {
|
|
2733
3078
|
void error;
|
|
@@ -2795,19 +3140,15 @@
|
|
|
2795
3140
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2796
3141
|
if (masks && masks.length) {
|
|
2797
3142
|
masks.forEach((mask) => {
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
preservedMasks.push(mask);
|
|
2808
|
-
}
|
|
2809
|
-
} catch (error) {
|
|
2810
|
-
this._reportWarning("applyCrop: failed to remove mask", error);
|
|
3143
|
+
mask.setCoords();
|
|
3144
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3145
|
+
const intersectsCrop = maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth && maskBounds.left + maskBounds.width > cropRegion.sourceX && maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight && maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3146
|
+
this._removeLabelForMask(mask);
|
|
3147
|
+
this.canvas.remove(mask);
|
|
3148
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3149
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3150
|
+
mask.set({ visible: true });
|
|
3151
|
+
preservedMasks.push(mask);
|
|
2811
3152
|
}
|
|
2812
3153
|
});
|
|
2813
3154
|
this._clearMaskPlacementMemory();
|
|
@@ -2815,7 +3156,8 @@
|
|
|
2815
3156
|
this.canvas.renderAll();
|
|
2816
3157
|
}
|
|
2817
3158
|
} catch (error) {
|
|
2818
|
-
this.
|
|
3159
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error);
|
|
3160
|
+
return;
|
|
2819
3161
|
}
|
|
2820
3162
|
this._removeCropRect();
|
|
2821
3163
|
this._cropMode = false;
|
|
@@ -2891,6 +3233,7 @@
|
|
|
2891
3233
|
const canUndo = this.historyManager?.canUndo();
|
|
2892
3234
|
const canRedo = this.historyManager?.canRedo();
|
|
2893
3235
|
const isInCropMode = !!this._cropMode;
|
|
3236
|
+
const isBusy = this.isBusy();
|
|
2894
3237
|
if (isInCropMode) {
|
|
2895
3238
|
for (const key of Object.keys(this.elements || {})) {
|
|
2896
3239
|
const element = this._getElement(key);
|
|
@@ -2903,23 +3246,27 @@
|
|
|
2903
3246
|
}
|
|
2904
3247
|
return;
|
|
2905
3248
|
}
|
|
2906
|
-
this._setDisabled("zoomInBtn", !hasImage ||
|
|
2907
|
-
this._setDisabled("zoomOutBtn", !hasImage ||
|
|
2908
|
-
this._setDisabled("rotateLeftBtn", !hasImage ||
|
|
2909
|
-
this._setDisabled("rotateRightBtn", !hasImage ||
|
|
2910
|
-
this._setDisabled("addMaskBtn", !hasImage ||
|
|
2911
|
-
this._setDisabled("removeMaskBtn", !hasSelectedMask ||
|
|
2912
|
-
this._setDisabled("removeAllMasksBtn", !hasMasks ||
|
|
2913
|
-
this._setDisabled("mergeBtn", !hasImage || !hasMasks ||
|
|
2914
|
-
this._setDisabled("downloadBtn", !hasImage ||
|
|
2915
|
-
this._setDisabled("resetBtn", !hasImage || isDefaultTransform ||
|
|
2916
|
-
this._setDisabled("undoBtn", !hasImage ||
|
|
2917
|
-
this._setDisabled("redoBtn", !hasImage ||
|
|
2918
|
-
this._setDisabled("cropBtn", !hasImage ||
|
|
3249
|
+
this._setDisabled("zoomInBtn", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3250
|
+
this._setDisabled("zoomOutBtn", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3251
|
+
this._setDisabled("rotateLeftBtn", !hasImage || isBusy);
|
|
3252
|
+
this._setDisabled("rotateRightBtn", !hasImage || isBusy);
|
|
3253
|
+
this._setDisabled("addMaskBtn", !hasImage || isBusy);
|
|
3254
|
+
this._setDisabled("removeMaskBtn", !hasSelectedMask || isBusy);
|
|
3255
|
+
this._setDisabled("removeAllMasksBtn", !hasMasks || isBusy);
|
|
3256
|
+
this._setDisabled("mergeBtn", !hasImage || !hasMasks || isBusy);
|
|
3257
|
+
this._setDisabled("downloadBtn", !hasImage || isBusy);
|
|
3258
|
+
this._setDisabled("resetBtn", !hasImage || isDefaultTransform || isBusy);
|
|
3259
|
+
this._setDisabled("undoBtn", !hasImage || isBusy || !canUndo);
|
|
3260
|
+
this._setDisabled("redoBtn", !hasImage || isBusy || !canRedo);
|
|
3261
|
+
this._setDisabled("cropBtn", !hasImage || isBusy);
|
|
2919
3262
|
this._setDisabled("applyCropBtn", true);
|
|
2920
3263
|
this._setDisabled("cancelCropBtn", true);
|
|
2921
|
-
this._setDisabled("
|
|
2922
|
-
this._setDisabled("
|
|
3264
|
+
this._setDisabled("scaleRate", !hasImage || isBusy);
|
|
3265
|
+
this._setDisabled("rotationLeftInput", !hasImage || isBusy);
|
|
3266
|
+
this._setDisabled("rotationRightInput", !hasImage || isBusy);
|
|
3267
|
+
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3268
|
+
this._setDisabled("imageInput", isBusy);
|
|
3269
|
+
this._setDisabled("uploadArea", isBusy);
|
|
2923
3270
|
}
|
|
2924
3271
|
/**
|
|
2925
3272
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
@@ -2935,12 +3282,16 @@
|
|
|
2935
3282
|
element.disabled = !!disabled;
|
|
2936
3283
|
return;
|
|
2937
3284
|
}
|
|
3285
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3286
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3287
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || "");
|
|
3288
|
+
}
|
|
2938
3289
|
if (disabled) {
|
|
2939
3290
|
element.setAttribute("aria-disabled", "true");
|
|
2940
3291
|
element.style.pointerEvents = "none";
|
|
2941
3292
|
} else {
|
|
2942
3293
|
element.removeAttribute("aria-disabled");
|
|
2943
|
-
element.style.pointerEvents = "";
|
|
3294
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? "";
|
|
2944
3295
|
}
|
|
2945
3296
|
}
|
|
2946
3297
|
_isElementDisabled(element) {
|
|
@@ -3026,6 +3377,9 @@
|
|
|
3026
3377
|
if (this.animationQueue) {
|
|
3027
3378
|
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3028
3379
|
}
|
|
3380
|
+
this._isLoading = false;
|
|
3381
|
+
this._activeOperationName = null;
|
|
3382
|
+
this._activeOperationToken = null;
|
|
3029
3383
|
try {
|
|
3030
3384
|
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3031
3385
|
const element = this._getElement(key);
|
|
@@ -3087,17 +3441,20 @@
|
|
|
3087
3441
|
}
|
|
3088
3442
|
this._handlersByElementKey = {};
|
|
3089
3443
|
this._elementCache = {};
|
|
3444
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3090
3445
|
this._clearMaskPlacementMemory();
|
|
3091
3446
|
this.originalImage = null;
|
|
3092
3447
|
this.baseImageScale = 1;
|
|
3093
3448
|
this.currentScale = 1;
|
|
3094
3449
|
this.currentRotation = 0;
|
|
3095
3450
|
this.isAnimating = false;
|
|
3451
|
+
this._isLoading = false;
|
|
3096
3452
|
this._cropMode = false;
|
|
3097
3453
|
this._cropRect = null;
|
|
3098
3454
|
this._cropHandlers = [];
|
|
3099
3455
|
this._cropPrevEvented = null;
|
|
3100
3456
|
this._prevSelectionSetting = void 0;
|
|
3457
|
+
this._lastContainerViewportSize = null;
|
|
3101
3458
|
this._initialized = false;
|
|
3102
3459
|
}
|
|
3103
3460
|
};
|
|
@@ -3109,6 +3466,7 @@
|
|
|
3109
3466
|
this.animationTasks = [];
|
|
3110
3467
|
this.isRunning = false;
|
|
3111
3468
|
this.currentTask = null;
|
|
3469
|
+
this._generation = 0;
|
|
3112
3470
|
}
|
|
3113
3471
|
/**
|
|
3114
3472
|
* Adds an animation function to the queue.
|
|
@@ -3128,6 +3486,7 @@
|
|
|
3128
3486
|
return this.isRunning || this.animationTasks.length > 0;
|
|
3129
3487
|
}
|
|
3130
3488
|
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3489
|
+
this._generation += 1;
|
|
3131
3490
|
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3132
3491
|
const tasks = [
|
|
3133
3492
|
...this.currentTask ? [this.currentTask] : [],
|
|
@@ -3149,26 +3508,33 @@
|
|
|
3149
3508
|
*/
|
|
3150
3509
|
async _drainQueue() {
|
|
3151
3510
|
if (this.isRunning) return;
|
|
3511
|
+
const generation = this._generation;
|
|
3152
3512
|
this.isRunning = true;
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
task.isSettled
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
task.isSettled
|
|
3165
|
-
|
|
3513
|
+
try {
|
|
3514
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
3515
|
+
const task = this.animationTasks.shift();
|
|
3516
|
+
this.currentTask = task;
|
|
3517
|
+
try {
|
|
3518
|
+
const result = await task.animationFn();
|
|
3519
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3520
|
+
task.isSettled = true;
|
|
3521
|
+
task.resolve(result);
|
|
3522
|
+
}
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3525
|
+
task.isSettled = true;
|
|
3526
|
+
task.reject(error);
|
|
3527
|
+
}
|
|
3528
|
+
} finally {
|
|
3529
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3166
3530
|
}
|
|
3167
|
-
}
|
|
3168
|
-
|
|
3531
|
+
}
|
|
3532
|
+
} finally {
|
|
3533
|
+
if (generation === this._generation) {
|
|
3534
|
+
this.isRunning = false;
|
|
3535
|
+
this.currentTask = null;
|
|
3169
3536
|
}
|
|
3170
3537
|
}
|
|
3171
|
-
this.isRunning = false;
|
|
3172
3538
|
}
|
|
3173
3539
|
};
|
|
3174
3540
|
var Command = class {
|
|
@@ -3213,9 +3579,9 @@
|
|
|
3213
3579
|
execute(command) {
|
|
3214
3580
|
const result = command.execute();
|
|
3215
3581
|
if (result && typeof result.then === "function") {
|
|
3216
|
-
return Promise.resolve(result).then(() => {
|
|
3582
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3217
3583
|
this.push(command);
|
|
3218
|
-
});
|
|
3584
|
+
}));
|
|
3219
3585
|
}
|
|
3220
3586
|
this.push(command);
|
|
3221
3587
|
return result;
|