@overtone-art/canvas-editor-core 0.2.6 → 0.2.7
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/LICENSE +21 -0
- package/dist/chunk-NINRTPOJ.mjs +104 -0
- package/dist/chunk-NINRTPOJ.mjs.map +1 -0
- package/dist/index.d.mts +144 -225
- package/dist/index.d.ts +144 -225
- package/dist/index.global.js +506 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1109 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1020 -92
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +37 -0
- package/dist/node.d.ts +37 -0
- package/dist/node.js +281 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +233 -0
- package/dist/node.mjs.map +1 -0
- package/dist/types-bOY7oYVq.d.mts +383 -0
- package/dist/types-bOY7oYVq.d.ts +383 -0
- package/package.json +13 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,27 +15,23 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
31
21
|
var index_exports = {};
|
|
32
22
|
__export(index_exports, {
|
|
23
|
+
CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
|
|
33
24
|
CanvasEditor: () => CanvasEditor,
|
|
34
25
|
CropController: () => CropController,
|
|
35
26
|
DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
|
|
36
27
|
EventEmitter: () => EventEmitter,
|
|
28
|
+
FontRegistry: () => FontRegistry,
|
|
37
29
|
HistoryManager: () => HistoryManager,
|
|
38
30
|
Layer: () => Layer,
|
|
39
31
|
LayerManager: () => LayerManager,
|
|
32
|
+
LicenseManager: () => LicenseManager,
|
|
40
33
|
PatternManager: () => PatternManager,
|
|
34
|
+
ProjectManager: () => ProjectManager,
|
|
41
35
|
SnapManager: () => SnapManager,
|
|
42
36
|
UnitConverter: () => UnitConverter,
|
|
43
37
|
applyPatternLocks: () => applyPatternLocks,
|
|
@@ -45,15 +39,22 @@ __export(index_exports, {
|
|
|
45
39
|
captureLocks: () => captureLocks,
|
|
46
40
|
clamp: () => clamp,
|
|
47
41
|
clearPatternImageCache: () => clearPatternImageCache,
|
|
42
|
+
computeCoverPlacement: () => computeCoverPlacement,
|
|
43
|
+
computePrintAreaClip: () => computePrintAreaClip,
|
|
48
44
|
computeTilePositions: () => computeTilePositions,
|
|
49
45
|
deserializeEditor: () => deserializeEditor,
|
|
50
46
|
drawTiles: () => drawTiles,
|
|
47
|
+
escapeXml: () => escapeXml,
|
|
51
48
|
exportDataURL: () => exportDataURL,
|
|
49
|
+
exportMockup: () => exportMockup,
|
|
52
50
|
exportPNG: () => exportPNG,
|
|
53
51
|
exportSVG: () => exportSVG,
|
|
54
52
|
generateId: () => generateId,
|
|
53
|
+
isCssColor: () => isCssColor,
|
|
54
|
+
loadPatternImage: () => loadPatternImage,
|
|
55
55
|
restoreLocks: () => restoreLocks,
|
|
56
56
|
round2: () => round2,
|
|
57
|
+
sanitizeSvg: () => sanitizeSvg,
|
|
57
58
|
serializeEditor: () => serializeEditor
|
|
58
59
|
});
|
|
59
60
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -151,6 +152,10 @@ var LayerManager = class {
|
|
|
151
152
|
canvas;
|
|
152
153
|
events;
|
|
153
154
|
layers = [];
|
|
155
|
+
onPropertyChanged;
|
|
156
|
+
setHistoryCallback(callback) {
|
|
157
|
+
this.onPropertyChanged = callback;
|
|
158
|
+
}
|
|
154
159
|
add(type, fabricObject, name, id) {
|
|
155
160
|
const layer = new Layer(type, fabricObject, name, id);
|
|
156
161
|
this.layers.push(layer);
|
|
@@ -167,12 +172,39 @@ var LayerManager = class {
|
|
|
167
172
|
this.layers.splice(index, 1);
|
|
168
173
|
this.events.emit("layer:removed", { layerId: id });
|
|
169
174
|
this.emitChanged();
|
|
175
|
+
this.onPropertyChanged?.();
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/** Replace a layer's render object while preserving its immutable ID and panel state. */
|
|
179
|
+
replaceObject(id, fabricObject) {
|
|
180
|
+
const layer = this.get(id);
|
|
181
|
+
if (!layer || layer.fabricObject === fabricObject) return false;
|
|
182
|
+
const previous = layer.fabricObject;
|
|
183
|
+
const stackIndex = this.canvas.getObjects().indexOf(previous);
|
|
184
|
+
const wasActive = this.canvas.getActiveObject() === previous;
|
|
185
|
+
this.canvas.remove(previous);
|
|
186
|
+
layer.fabricObject = fabricObject;
|
|
187
|
+
fabricObject._layerId = id;
|
|
188
|
+
fabricObject.set({
|
|
189
|
+
visible: layer.visible,
|
|
190
|
+
opacity: layer.opacity,
|
|
191
|
+
selectable: !layer.locked,
|
|
192
|
+
evented: !layer.locked
|
|
193
|
+
});
|
|
194
|
+
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
195
|
+
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
196
|
+
this.canvas.requestRenderAll();
|
|
197
|
+
this.events.emit("layer:modified", { layerId: id });
|
|
198
|
+
this.emitChanged();
|
|
199
|
+
this.onPropertyChanged?.();
|
|
170
200
|
return true;
|
|
171
201
|
}
|
|
172
202
|
reorder(id, newIndex) {
|
|
173
203
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
174
204
|
if (oldIndex === -1) return false;
|
|
175
|
-
|
|
205
|
+
if (!Number.isFinite(newIndex)) return false;
|
|
206
|
+
const clamped = Math.max(0, Math.min(this.layers.length - 1, Math.round(newIndex)));
|
|
207
|
+
if (oldIndex === clamped) return false;
|
|
176
208
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
177
209
|
this.layers.splice(clamped, 0, layer);
|
|
178
210
|
this.layers.forEach((l, i) => {
|
|
@@ -180,6 +212,7 @@ var LayerManager = class {
|
|
|
180
212
|
});
|
|
181
213
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
182
214
|
this.emitChanged();
|
|
215
|
+
this.onPropertyChanged?.();
|
|
183
216
|
return true;
|
|
184
217
|
}
|
|
185
218
|
select(id) {
|
|
@@ -210,33 +243,42 @@ var LayerManager = class {
|
|
|
210
243
|
setVisibility(id, visible) {
|
|
211
244
|
const layer = this.get(id);
|
|
212
245
|
if (!layer) return;
|
|
246
|
+
if (layer.visible === visible) return;
|
|
213
247
|
layer.visible = visible;
|
|
214
248
|
layer.fabricObject.visible = visible;
|
|
215
249
|
this.canvas.requestRenderAll();
|
|
216
250
|
this.emitChanged();
|
|
251
|
+
this.onPropertyChanged?.();
|
|
217
252
|
}
|
|
218
253
|
setLocked(id, locked) {
|
|
219
254
|
const layer = this.get(id);
|
|
220
255
|
if (!layer) return;
|
|
256
|
+
if (layer.locked === locked) return;
|
|
221
257
|
layer.locked = locked;
|
|
222
258
|
layer.fabricObject.selectable = !locked;
|
|
223
259
|
layer.fabricObject.evented = !locked;
|
|
224
260
|
this.canvas.requestRenderAll();
|
|
225
261
|
this.emitChanged();
|
|
262
|
+
this.onPropertyChanged?.();
|
|
226
263
|
}
|
|
227
264
|
setOpacity(id, opacity) {
|
|
228
265
|
const layer = this.get(id);
|
|
229
|
-
if (!layer) return;
|
|
230
|
-
|
|
231
|
-
layer.
|
|
266
|
+
if (!layer || !Number.isFinite(opacity)) return;
|
|
267
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
268
|
+
if (layer.opacity === next) return;
|
|
269
|
+
layer.opacity = next;
|
|
270
|
+
layer.fabricObject.opacity = next;
|
|
232
271
|
this.canvas.requestRenderAll();
|
|
233
272
|
this.emitChanged();
|
|
273
|
+
this.onPropertyChanged?.();
|
|
234
274
|
}
|
|
235
275
|
setName(id, name) {
|
|
236
276
|
const layer = this.get(id);
|
|
237
277
|
if (!layer) return;
|
|
278
|
+
if (layer.name === name) return;
|
|
238
279
|
layer.name = name;
|
|
239
280
|
this.emitChanged();
|
|
281
|
+
this.onPropertyChanged?.();
|
|
240
282
|
}
|
|
241
283
|
clear() {
|
|
242
284
|
for (const layer of this.layers) {
|
|
@@ -260,57 +302,124 @@ var HistoryManager = class {
|
|
|
260
302
|
undoStack = [];
|
|
261
303
|
redoStack = [];
|
|
262
304
|
maxSize;
|
|
305
|
+
maxBytes;
|
|
263
306
|
paused = false;
|
|
264
307
|
debounceTimer = null;
|
|
265
308
|
debounceMs;
|
|
266
309
|
getState;
|
|
267
310
|
restoreState;
|
|
268
311
|
events;
|
|
312
|
+
transactionDepth = 0;
|
|
313
|
+
transactionDirty = false;
|
|
269
314
|
constructor(opts) {
|
|
270
315
|
this.getState = opts.getState;
|
|
271
316
|
this.restoreState = opts.restoreState;
|
|
272
317
|
this.events = opts.events;
|
|
273
318
|
this.maxSize = opts.maxSize ?? 50;
|
|
319
|
+
this.maxBytes = opts.maxBytes ?? 50 * 1024 * 1024;
|
|
274
320
|
this.debounceMs = opts.debounceMs ?? 300;
|
|
275
321
|
}
|
|
276
322
|
save() {
|
|
277
323
|
if (this.paused) return;
|
|
324
|
+
if (this.transactionDepth > 0) {
|
|
325
|
+
this.transactionDirty = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
278
328
|
if (this.debounceTimer) {
|
|
279
329
|
clearTimeout(this.debounceTimer);
|
|
280
330
|
}
|
|
281
331
|
this.debounceTimer = setTimeout(() => {
|
|
282
332
|
this.saveImmediate();
|
|
283
333
|
}, this.debounceMs);
|
|
334
|
+
this.emitChanged();
|
|
284
335
|
}
|
|
285
336
|
saveImmediate() {
|
|
286
|
-
if (this.paused)
|
|
337
|
+
if (this.paused) {
|
|
338
|
+
this.cancelPending();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (this.transactionDepth > 0) {
|
|
342
|
+
this.transactionDirty = true;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.cancelPending();
|
|
287
346
|
const state = this.getState();
|
|
347
|
+
if (this.undoStack.at(-1) === state) {
|
|
348
|
+
this.emitChanged();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
288
351
|
this.undoStack.push(state);
|
|
289
|
-
|
|
352
|
+
while (this.undoStack.length > this.maxSize) {
|
|
290
353
|
this.undoStack.shift();
|
|
291
354
|
}
|
|
292
355
|
this.redoStack = [];
|
|
356
|
+
this.trimToBudget();
|
|
357
|
+
this.events.emit("history:snapshot", {
|
|
358
|
+
bytes: state.length * 2,
|
|
359
|
+
totalBytes: this.snapshotBytes(),
|
|
360
|
+
entries: this.undoStack.length
|
|
361
|
+
});
|
|
293
362
|
this.emitChanged();
|
|
294
363
|
}
|
|
364
|
+
beginTransaction() {
|
|
365
|
+
if (this.transactionDepth === 0 && this.debounceTimer) this.saveImmediate();
|
|
366
|
+
this.transactionDepth += 1;
|
|
367
|
+
}
|
|
368
|
+
endTransaction() {
|
|
369
|
+
if (this.transactionDepth === 0) return;
|
|
370
|
+
this.transactionDepth -= 1;
|
|
371
|
+
if (this.transactionDepth === 0 && this.transactionDirty) {
|
|
372
|
+
this.transactionDirty = false;
|
|
373
|
+
this.saveImmediate();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async transaction(operation) {
|
|
377
|
+
this.beginTransaction();
|
|
378
|
+
try {
|
|
379
|
+
return await operation();
|
|
380
|
+
} finally {
|
|
381
|
+
this.endTransaction();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
295
384
|
async undo() {
|
|
296
385
|
this.cancelPending();
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
386
|
+
const current = this.getState();
|
|
387
|
+
const committed = this.undoStack.at(-1);
|
|
388
|
+
if (!committed) return;
|
|
389
|
+
const currentIsCommitted = current === committed;
|
|
390
|
+
if (currentIsCommitted && this.undoStack.length < 2) return;
|
|
391
|
+
const target = currentIsCommitted ? this.undoStack[this.undoStack.length - 2] : committed;
|
|
300
392
|
this.paused = true;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
393
|
+
try {
|
|
394
|
+
await this.restoreState(target);
|
|
395
|
+
if (currentIsCommitted) this.undoStack.pop();
|
|
396
|
+
this.redoStack.push(current);
|
|
397
|
+
this.trimToBudget();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.events.emit("error", { message: "Failed to undo the last change", error });
|
|
400
|
+
throw error;
|
|
401
|
+
} finally {
|
|
402
|
+
this.paused = false;
|
|
403
|
+
this.emitChanged();
|
|
404
|
+
}
|
|
304
405
|
}
|
|
305
406
|
async redo() {
|
|
306
407
|
this.cancelPending();
|
|
307
|
-
const state = this.redoStack.
|
|
408
|
+
const state = this.redoStack.at(-1);
|
|
308
409
|
if (!state) return;
|
|
309
|
-
this.undoStack.push(this.getState());
|
|
310
410
|
this.paused = true;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
411
|
+
try {
|
|
412
|
+
await this.restoreState(state);
|
|
413
|
+
this.redoStack.pop();
|
|
414
|
+
if (this.undoStack.at(-1) !== state) this.undoStack.push(state);
|
|
415
|
+
this.trimToBudget();
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.events.emit("error", { message: "Failed to redo the last change", error });
|
|
418
|
+
throw error;
|
|
419
|
+
} finally {
|
|
420
|
+
this.paused = false;
|
|
421
|
+
this.emitChanged();
|
|
422
|
+
}
|
|
314
423
|
}
|
|
315
424
|
/** True while a restore (undo/redo/deserialize) is in flight. Managers that
|
|
316
425
|
* react to canvas events should skip mutating history during this window. */
|
|
@@ -324,16 +433,20 @@ var HistoryManager = class {
|
|
|
324
433
|
this.paused = false;
|
|
325
434
|
}
|
|
326
435
|
canUndo() {
|
|
327
|
-
return this.undoStack.length >
|
|
436
|
+
return this.undoStack.length > 1 || this.debounceTimer !== null;
|
|
328
437
|
}
|
|
329
438
|
canRedo() {
|
|
330
439
|
return this.redoStack.length > 0;
|
|
331
440
|
}
|
|
332
441
|
clear() {
|
|
442
|
+
this.cancelPending();
|
|
333
443
|
this.undoStack = [];
|
|
334
444
|
this.redoStack = [];
|
|
335
445
|
this.emitChanged();
|
|
336
446
|
}
|
|
447
|
+
getSnapshotBytes() {
|
|
448
|
+
return this.snapshotBytes();
|
|
449
|
+
}
|
|
337
450
|
cancelPending() {
|
|
338
451
|
if (this.debounceTimer) {
|
|
339
452
|
clearTimeout(this.debounceTimer);
|
|
@@ -341,9 +454,9 @@ var HistoryManager = class {
|
|
|
341
454
|
}
|
|
342
455
|
}
|
|
343
456
|
dispose() {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
457
|
+
this.cancelPending();
|
|
458
|
+
this.transactionDepth = 0;
|
|
459
|
+
this.transactionDirty = false;
|
|
347
460
|
}
|
|
348
461
|
emitChanged() {
|
|
349
462
|
this.events.emit("history:changed", {
|
|
@@ -351,6 +464,20 @@ var HistoryManager = class {
|
|
|
351
464
|
canRedo: this.canRedo()
|
|
352
465
|
});
|
|
353
466
|
}
|
|
467
|
+
snapshotBytes() {
|
|
468
|
+
return [...this.undoStack, ...this.redoStack].reduce(
|
|
469
|
+
(total, state) => total + state.length * 2,
|
|
470
|
+
0
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
trimToBudget() {
|
|
474
|
+
while (this.undoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
475
|
+
this.undoStack.shift();
|
|
476
|
+
}
|
|
477
|
+
while (this.redoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
478
|
+
this.redoStack.shift();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
354
481
|
};
|
|
355
482
|
|
|
356
483
|
// src/snapping.ts
|
|
@@ -570,6 +697,10 @@ var CropController = class {
|
|
|
570
697
|
top: imgTop + (cropY - (image.cropY ?? 0)) * scaleY
|
|
571
698
|
});
|
|
572
699
|
image.setCoords();
|
|
700
|
+
if (s.prevAngle) {
|
|
701
|
+
image.rotate(s.prevAngle);
|
|
702
|
+
image.setCoords();
|
|
703
|
+
}
|
|
573
704
|
this.finish();
|
|
574
705
|
this.history.save();
|
|
575
706
|
}
|
|
@@ -604,14 +735,18 @@ var STROKE2 = "#22c55e";
|
|
|
604
735
|
var import_fabric2 = require("fabric");
|
|
605
736
|
var MAX_TILES_PER_AXIS = 200;
|
|
606
737
|
var PatternManager = class {
|
|
607
|
-
constructor(canvas, layers, history) {
|
|
738
|
+
constructor(canvas, layers, history, events, sourceResolver) {
|
|
608
739
|
this.canvas = canvas;
|
|
609
740
|
this.layers = layers;
|
|
610
741
|
this.history = history;
|
|
742
|
+
this.events = events;
|
|
743
|
+
this.sourceResolver = sourceResolver;
|
|
611
744
|
}
|
|
612
745
|
canvas;
|
|
613
746
|
layers;
|
|
614
747
|
history;
|
|
748
|
+
events;
|
|
749
|
+
sourceResolver;
|
|
615
750
|
// Per-layer task chain. apply()/disable() both await an async setSrc on the
|
|
616
751
|
// same fabric image; running two concurrently lets their setSrc resolutions
|
|
617
752
|
// interleave (wrong image installed, original lost). Serialising per layer
|
|
@@ -659,6 +794,9 @@ var PatternManager = class {
|
|
|
659
794
|
throw err;
|
|
660
795
|
}
|
|
661
796
|
this.history.save();
|
|
797
|
+
}).catch((error) => {
|
|
798
|
+
this.events.emit("error", { message: "Failed to apply image pattern", error });
|
|
799
|
+
throw error;
|
|
662
800
|
});
|
|
663
801
|
}
|
|
664
802
|
/**
|
|
@@ -714,6 +852,9 @@ var PatternManager = class {
|
|
|
714
852
|
delete layer.meta.pattern;
|
|
715
853
|
this.canvas.requestRenderAll();
|
|
716
854
|
this.history.save();
|
|
855
|
+
}).catch((error) => {
|
|
856
|
+
this.events.emit("error", { message: "Failed to clear image pattern", error });
|
|
857
|
+
throw error;
|
|
717
858
|
});
|
|
718
859
|
}
|
|
719
860
|
/** Run `task` after any in-flight work for this layer, regardless of outcome. */
|
|
@@ -743,7 +884,8 @@ var PatternManager = class {
|
|
|
743
884
|
cw,
|
|
744
885
|
ch,
|
|
745
886
|
tileW,
|
|
746
|
-
tileH
|
|
887
|
+
tileH,
|
|
888
|
+
this.sourceResolver
|
|
747
889
|
);
|
|
748
890
|
await image.setSrc(dataUrl);
|
|
749
891
|
image.set({
|
|
@@ -814,14 +956,23 @@ function elementToDataURL(image) {
|
|
|
814
956
|
}
|
|
815
957
|
var IMAGE_CACHE_MAX = 16;
|
|
816
958
|
var imageCache = /* @__PURE__ */ new Map();
|
|
817
|
-
function
|
|
959
|
+
function loadPatternImage(src, resolver) {
|
|
818
960
|
const cached = imageCache.get(src);
|
|
819
961
|
if (cached) {
|
|
820
962
|
imageCache.delete(src);
|
|
821
963
|
imageCache.set(src, cached);
|
|
822
964
|
return cached;
|
|
823
965
|
}
|
|
824
|
-
const promise = decodeImage(src)
|
|
966
|
+
const promise = decodeImage(src).catch(async (originalError) => {
|
|
967
|
+
if (!resolver) throw originalError;
|
|
968
|
+
const resolved = await resolver(src);
|
|
969
|
+
if (!resolved || resolved === src) {
|
|
970
|
+
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
971
|
+
cause: originalError
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
return decodeImage(resolved);
|
|
975
|
+
});
|
|
825
976
|
promise.catch(() => {
|
|
826
977
|
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
827
978
|
});
|
|
@@ -844,8 +995,8 @@ function decodeImage(src) {
|
|
|
844
995
|
function clearPatternImageCache() {
|
|
845
996
|
imageCache.clear();
|
|
846
997
|
}
|
|
847
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH) {
|
|
848
|
-
const img = await
|
|
998
|
+
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
999
|
+
const img = await loadPatternImage(src, sourceResolver);
|
|
849
1000
|
const off = document.createElement("canvas");
|
|
850
1001
|
off.width = Math.max(1, Math.round(targetW));
|
|
851
1002
|
off.height = Math.max(1, Math.round(targetH));
|
|
@@ -905,16 +1056,18 @@ function mod2(n) {
|
|
|
905
1056
|
// src/utils/units.ts
|
|
906
1057
|
var MM_PER_INCH = 25.4;
|
|
907
1058
|
var UnitConverter = class {
|
|
1059
|
+
unit;
|
|
1060
|
+
dpi;
|
|
908
1061
|
constructor(unit = "px", dpi = 72) {
|
|
909
1062
|
this.unit = unit;
|
|
910
|
-
this.dpi =
|
|
1063
|
+
this.dpi = 72;
|
|
1064
|
+
this.setDpi(dpi);
|
|
911
1065
|
}
|
|
912
|
-
unit;
|
|
913
|
-
dpi;
|
|
914
1066
|
setUnit(unit) {
|
|
915
1067
|
this.unit = unit;
|
|
916
1068
|
}
|
|
917
1069
|
setDpi(dpi) {
|
|
1070
|
+
if (!Number.isFinite(dpi) || dpi <= 0) throw new Error("DPI must be a positive number");
|
|
918
1071
|
this.dpi = dpi;
|
|
919
1072
|
}
|
|
920
1073
|
getUnit() {
|
|
@@ -949,38 +1102,72 @@ var UnitConverter = class {
|
|
|
949
1102
|
|
|
950
1103
|
// src/serialization.ts
|
|
951
1104
|
var import_fabric3 = require("fabric");
|
|
952
|
-
|
|
1105
|
+
|
|
1106
|
+
// src/utils/color.ts
|
|
1107
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1108
|
+
function isCssColor(value, allowEmpty = false) {
|
|
1109
|
+
if (typeof value !== "string") return false;
|
|
1110
|
+
const trimmed = value.trim();
|
|
1111
|
+
if (!trimmed) return allowEmpty;
|
|
1112
|
+
return CSS_COLOR.test(trimmed);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/serialization.ts
|
|
1116
|
+
var VERSION = "2.0.0";
|
|
953
1117
|
function serializeEditor(editor) {
|
|
954
1118
|
return {
|
|
955
1119
|
version: VERSION,
|
|
956
1120
|
canvas: {
|
|
957
1121
|
width: editor.canvas.getWidth(),
|
|
958
|
-
height: editor.canvas.getHeight()
|
|
1122
|
+
height: editor.canvas.getHeight(),
|
|
1123
|
+
unit: editor.units.getUnit(),
|
|
1124
|
+
dpi: editor.units.getDpi()
|
|
959
1125
|
},
|
|
960
1126
|
layers: editor.layers.getAll().map((layer) => layer.serialize()),
|
|
961
1127
|
// The configured design background, not the live canvas value (which is
|
|
962
1128
|
// forced transparent while a mockup preview is active).
|
|
963
1129
|
background: editor.getDesignBackground(),
|
|
1130
|
+
backgroundImage: editor.getDesignBackgroundImage() ? editor.getDesignBackgroundImage().toObject() : null,
|
|
964
1131
|
mockup: editor.getMockup()
|
|
965
1132
|
};
|
|
966
1133
|
}
|
|
967
1134
|
async function deserializeEditor(editor, state) {
|
|
1135
|
+
if (!state || !state.canvas || !Array.isArray(state.layers)) {
|
|
1136
|
+
throw new Error("Invalid editor state");
|
|
1137
|
+
}
|
|
1138
|
+
if (!Number.isFinite(state.canvas.width) || !Number.isFinite(state.canvas.height) || state.canvas.width <= 0 || state.canvas.height <= 0 || state.canvas.unit !== void 0 && !["px", "mm", "in"].includes(state.canvas.unit) || state.canvas.dpi !== void 0 && (!Number.isFinite(state.canvas.dpi) || state.canvas.dpi <= 0)) {
|
|
1139
|
+
throw new Error("Invalid editor canvas settings");
|
|
1140
|
+
}
|
|
1141
|
+
const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
|
|
1142
|
+
if (!Number.isFinite(major) || major > 2) {
|
|
1143
|
+
throw new Error(`Unsupported editor state version: ${state.version}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (state.background !== void 0 && !isCssColor(state.background, true)) {
|
|
1146
|
+
throw new Error("Invalid editor background color");
|
|
1147
|
+
}
|
|
1148
|
+
const staged = await Promise.all(
|
|
1149
|
+
state.layers.map(async (serialized) => ({
|
|
1150
|
+
serialized,
|
|
1151
|
+
fabricObject: (await import_fabric3.util.enlivenObjects([serialized.fabricObject]))[0]
|
|
1152
|
+
}))
|
|
1153
|
+
);
|
|
1154
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric3.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1155
|
+
editor.crop.cancel();
|
|
968
1156
|
editor.layers.clear();
|
|
1157
|
+
if (state.canvas.unit) editor.units.setUnit(state.canvas.unit);
|
|
1158
|
+
if (state.canvas.dpi !== void 0) editor.units.setDpi(state.canvas.dpi);
|
|
969
1159
|
editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
|
|
970
|
-
if (state.background) {
|
|
1160
|
+
if (state.background !== void 0) {
|
|
971
1161
|
editor.setBackground(state.background);
|
|
972
1162
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
await restoreLayer(editor, serializedLayer);
|
|
1163
|
+
editor.setBackgroundImageObject(stagedBackground, false);
|
|
1164
|
+
editor.setMockup(state.mockup ?? null);
|
|
1165
|
+
for (const item of staged) {
|
|
1166
|
+
restoreLayer(editor, item.serialized, item.fabricObject);
|
|
978
1167
|
}
|
|
979
1168
|
editor.canvas.requestRenderAll();
|
|
980
1169
|
}
|
|
981
|
-
|
|
982
|
-
const objects = await import_fabric3.util.enlivenObjects([serialized.fabricObject]);
|
|
983
|
-
const fabricObject = objects[0];
|
|
1170
|
+
function restoreLayer(editor, serialized, fabricObject) {
|
|
984
1171
|
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
985
1172
|
if (serialized.meta) {
|
|
986
1173
|
layer.meta = serialized.meta;
|
|
@@ -998,15 +1185,92 @@ async function restoreLayer(editor, serialized) {
|
|
|
998
1185
|
}
|
|
999
1186
|
|
|
1000
1187
|
// src/export.ts
|
|
1188
|
+
function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
|
|
1189
|
+
const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
|
|
1190
|
+
const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));
|
|
1191
|
+
const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));
|
|
1192
|
+
const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));
|
|
1193
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
1194
|
+
}
|
|
1195
|
+
function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHeight) {
|
|
1196
|
+
if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
|
|
1197
|
+
throw new Error("Cover dimensions must be positive");
|
|
1198
|
+
}
|
|
1199
|
+
const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
|
|
1200
|
+
const width = sourceWidth * scale;
|
|
1201
|
+
const height = sourceHeight * scale;
|
|
1202
|
+
return {
|
|
1203
|
+
left: (targetWidth - width) / 2,
|
|
1204
|
+
top: (targetHeight - height) / 2,
|
|
1205
|
+
width,
|
|
1206
|
+
height
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function canvasElementToBlob(output, format, quality) {
|
|
1210
|
+
const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
|
|
1211
|
+
return new Promise((resolve, reject) => {
|
|
1212
|
+
output.toBlob(
|
|
1213
|
+
(blob) => blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`)),
|
|
1214
|
+
mime,
|
|
1215
|
+
quality
|
|
1216
|
+
);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1001
1219
|
async function exportPNG(canvas, options = {}) {
|
|
1002
1220
|
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1221
|
+
const output = canvas.toCanvasElement(multiplier);
|
|
1222
|
+
return canvasElementToBlob(output, format, quality);
|
|
1223
|
+
}
|
|
1224
|
+
async function exportMockup(canvas, mockup, options = {}) {
|
|
1225
|
+
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1226
|
+
const design = canvas.toCanvasElement(multiplier);
|
|
1227
|
+
const output = design.ownerDocument.createElement("canvas");
|
|
1228
|
+
output.width = design.width;
|
|
1229
|
+
output.height = design.height;
|
|
1230
|
+
const context = output.getContext("2d");
|
|
1231
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1232
|
+
const loadImage = (url) => new Promise((resolve, reject) => {
|
|
1233
|
+
const element = new Image();
|
|
1234
|
+
element.crossOrigin = "anonymous";
|
|
1235
|
+
element.onload = () => resolve(element);
|
|
1236
|
+
element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
|
|
1237
|
+
element.src = url;
|
|
1007
1238
|
});
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1239
|
+
const drawCover = (image) => {
|
|
1240
|
+
const placement = computeCoverPlacement(
|
|
1241
|
+
image.naturalWidth || image.width,
|
|
1242
|
+
image.naturalHeight || image.height,
|
|
1243
|
+
output.width,
|
|
1244
|
+
output.height
|
|
1245
|
+
);
|
|
1246
|
+
context.drawImage(image, placement.left, placement.top, placement.width, placement.height);
|
|
1247
|
+
};
|
|
1248
|
+
drawCover(await loadImage(mockup.image));
|
|
1249
|
+
context.save();
|
|
1250
|
+
if (mockup.printArea && mockup.clipToPrintArea !== false) {
|
|
1251
|
+
const clip = computePrintAreaClip(
|
|
1252
|
+
mockup.printArea,
|
|
1253
|
+
output.width / canvas.getWidth(),
|
|
1254
|
+
output.height / canvas.getHeight(),
|
|
1255
|
+
output.width,
|
|
1256
|
+
output.height
|
|
1257
|
+
);
|
|
1258
|
+
context.beginPath();
|
|
1259
|
+
context.rect(clip.left, clip.top, clip.width, clip.height);
|
|
1260
|
+
context.clip();
|
|
1261
|
+
}
|
|
1262
|
+
context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
|
|
1263
|
+
context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
|
|
1264
|
+
context.drawImage(design, 0, 0);
|
|
1265
|
+
context.restore();
|
|
1266
|
+
if (mockup.overlay) {
|
|
1267
|
+
context.save();
|
|
1268
|
+
context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
|
|
1269
|
+
context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
|
|
1270
|
+
drawCover(await loadImage(mockup.overlay.image));
|
|
1271
|
+
context.restore();
|
|
1272
|
+
}
|
|
1273
|
+
return canvasElementToBlob(output, format, quality);
|
|
1010
1274
|
}
|
|
1011
1275
|
function exportSVG(canvas) {
|
|
1012
1276
|
return canvas.toSVG();
|
|
@@ -1015,6 +1279,319 @@ function exportDataURL(canvas, format = "png", multiplier = 1) {
|
|
|
1015
1279
|
return canvas.toDataURL({ format, multiplier });
|
|
1016
1280
|
}
|
|
1017
1281
|
|
|
1282
|
+
// src/utils/svg.ts
|
|
1283
|
+
function escapeXml(value) {
|
|
1284
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1285
|
+
}
|
|
1286
|
+
function sanitizeSvg(svg) {
|
|
1287
|
+
const document2 = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
1288
|
+
if (document2.querySelector("parsererror")) throw new Error("Invalid template SVG");
|
|
1289
|
+
document2.querySelectorAll("script, foreignObject, iframe, object, embed, link, style").forEach((node) => node.remove());
|
|
1290
|
+
document2.querySelectorAll("*").forEach((node) => {
|
|
1291
|
+
for (const attribute of [...node.attributes]) {
|
|
1292
|
+
const name = attribute.name.toLowerCase();
|
|
1293
|
+
const value = attribute.value.trim().toLowerCase();
|
|
1294
|
+
const isLink = name === "href" || name === "xlink:href" || name === "src";
|
|
1295
|
+
const safeLink = value.startsWith("#") || /^data:image\/(?:png|jpeg|webp|gif);base64,/.test(value);
|
|
1296
|
+
if (name.startsWith("on") || isLink && !safeLink || /url\s*\(/.test(value) && !/url\s*\(\s*['"]?#/.test(value) || /(?:javascript:|expression\s*\()/.test(value)) {
|
|
1297
|
+
node.removeAttribute(attribute.name);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
return new XMLSerializer().serializeToString(document2.documentElement);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/fonts.ts
|
|
1305
|
+
function fontSource(source) {
|
|
1306
|
+
return /^(?:url|local)\(/.test(source.trim()) ? source : `url(${JSON.stringify(source)})`;
|
|
1307
|
+
}
|
|
1308
|
+
function mimeForSource(source) {
|
|
1309
|
+
const path = source.split(/[?#]/)[0].toLowerCase();
|
|
1310
|
+
if (path.endsWith(".woff2")) return "font/woff2";
|
|
1311
|
+
if (path.endsWith(".woff")) return "font/woff";
|
|
1312
|
+
if (path.endsWith(".otf")) return "font/otf";
|
|
1313
|
+
return "font/ttf";
|
|
1314
|
+
}
|
|
1315
|
+
function arrayBufferToBase64(buffer) {
|
|
1316
|
+
const bytes = new Uint8Array(buffer);
|
|
1317
|
+
let binary = "";
|
|
1318
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
1319
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
1320
|
+
}
|
|
1321
|
+
return btoa(binary);
|
|
1322
|
+
}
|
|
1323
|
+
function cssString(value) {
|
|
1324
|
+
return JSON.stringify(value).replace(/[<>&]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
|
|
1325
|
+
}
|
|
1326
|
+
var SAFE_WEIGHT = /^(?:normal|bold|bolder|lighter|[1-9]\d{0,2}(?:\s+[1-9]\d{0,2})?)$/i;
|
|
1327
|
+
var SAFE_STYLE = /^(?:normal|italic|oblique(?:\s+-?\d+(?:\.\d+)?deg)?)$/i;
|
|
1328
|
+
var SAFE_DISPLAY = /^(?:auto|block|swap|fallback|optional)$/i;
|
|
1329
|
+
function cssKeyword(value, pattern, fallback) {
|
|
1330
|
+
const trimmed = value?.trim();
|
|
1331
|
+
return trimmed && pattern.test(trimmed) ? trimmed : fallback;
|
|
1332
|
+
}
|
|
1333
|
+
function sourceUrl(source) {
|
|
1334
|
+
const trimmed = source.trim();
|
|
1335
|
+
if (trimmed.startsWith("data:")) return trimmed;
|
|
1336
|
+
const match = trimmed.match(/^url\(\s*(['"]?)(.*?)\1\s*\)/i);
|
|
1337
|
+
if (match) return match[2];
|
|
1338
|
+
if (/^local\(/i.test(trimmed)) return null;
|
|
1339
|
+
return trimmed;
|
|
1340
|
+
}
|
|
1341
|
+
function localName(source) {
|
|
1342
|
+
const match = source.trim().match(/^local\(\s*(['"]?)([^)"'{};]*)\1\s*\)$/i);
|
|
1343
|
+
return match ? match[2].trim() || null : null;
|
|
1344
|
+
}
|
|
1345
|
+
var FontRegistry = class {
|
|
1346
|
+
definitions = /* @__PURE__ */ new Map();
|
|
1347
|
+
loads = /* @__PURE__ */ new Map();
|
|
1348
|
+
register(definition) {
|
|
1349
|
+
if (!definition.family.trim() || !definition.source.trim()) {
|
|
1350
|
+
throw new Error("Font family and source are required");
|
|
1351
|
+
}
|
|
1352
|
+
this.definitions.set(definition.family, { ...definition });
|
|
1353
|
+
this.loads.delete(definition.family);
|
|
1354
|
+
}
|
|
1355
|
+
unregister(family) {
|
|
1356
|
+
this.loads.delete(family);
|
|
1357
|
+
return this.definitions.delete(family);
|
|
1358
|
+
}
|
|
1359
|
+
getAll() {
|
|
1360
|
+
return [...this.definitions.values()].map((definition) => ({ ...definition }));
|
|
1361
|
+
}
|
|
1362
|
+
load(family) {
|
|
1363
|
+
const cached = this.loads.get(family);
|
|
1364
|
+
if (cached) return cached;
|
|
1365
|
+
const definition = this.definitions.get(family);
|
|
1366
|
+
if (!definition) return Promise.reject(new Error(`Font is not registered: ${family}`));
|
|
1367
|
+
if (typeof FontFace === "undefined" || typeof document === "undefined") {
|
|
1368
|
+
return Promise.reject(new Error("Font loading requires a browser FontFace API"));
|
|
1369
|
+
}
|
|
1370
|
+
const promise = new FontFace(definition.family, fontSource(definition.source), {
|
|
1371
|
+
weight: definition.weight,
|
|
1372
|
+
style: definition.style,
|
|
1373
|
+
display: definition.display
|
|
1374
|
+
}).load().then((font) => {
|
|
1375
|
+
document.fonts.add(font);
|
|
1376
|
+
return font;
|
|
1377
|
+
});
|
|
1378
|
+
promise.catch(() => {
|
|
1379
|
+
if (this.loads.get(family) === promise) this.loads.delete(family);
|
|
1380
|
+
});
|
|
1381
|
+
this.loads.set(family, promise);
|
|
1382
|
+
return promise;
|
|
1383
|
+
}
|
|
1384
|
+
async ready() {
|
|
1385
|
+
if (this.definitions.size === 0) return;
|
|
1386
|
+
await Promise.all([...this.definitions.keys()].map((family) => this.load(family)));
|
|
1387
|
+
await document.fonts.ready;
|
|
1388
|
+
}
|
|
1389
|
+
async getEmbeddedCss() {
|
|
1390
|
+
const rules = await Promise.all(
|
|
1391
|
+
this.getAll().map(async (definition) => {
|
|
1392
|
+
const url = sourceUrl(definition.source);
|
|
1393
|
+
let cssSource;
|
|
1394
|
+
if (url && !url.startsWith("data:")) {
|
|
1395
|
+
const response = await fetch(url);
|
|
1396
|
+
if (!response.ok) throw new Error(`Failed to fetch font: ${url}`);
|
|
1397
|
+
const data = arrayBufferToBase64(await response.arrayBuffer());
|
|
1398
|
+
const mime = response.headers.get("content-type") || mimeForSource(url);
|
|
1399
|
+
cssSource = `url(${cssString(`data:${mime};base64,${data}`)})`;
|
|
1400
|
+
} else if (url) {
|
|
1401
|
+
cssSource = `url(${cssString(url)})`;
|
|
1402
|
+
} else {
|
|
1403
|
+
const name = localName(definition.source);
|
|
1404
|
+
if (!name) {
|
|
1405
|
+
throw new Error(`Unsupported font source for embedding: ${definition.family}`);
|
|
1406
|
+
}
|
|
1407
|
+
cssSource = `local(${cssString(name)})`;
|
|
1408
|
+
}
|
|
1409
|
+
const weight = cssKeyword(definition.weight, SAFE_WEIGHT, "normal");
|
|
1410
|
+
const style = cssKeyword(definition.style, SAFE_STYLE, "normal");
|
|
1411
|
+
const display = cssKeyword(definition.display, SAFE_DISPLAY, "swap");
|
|
1412
|
+
return `@font-face{font-family:${cssString(definition.family)};src:${cssSource};font-weight:${weight};font-style:${style};font-display:${display}}`;
|
|
1413
|
+
})
|
|
1414
|
+
);
|
|
1415
|
+
return rules.join("\n");
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
// src/licensing.ts
|
|
1420
|
+
function domainMatches(hostname, pattern) {
|
|
1421
|
+
const host = hostname.toLowerCase();
|
|
1422
|
+
const expected = pattern.toLowerCase();
|
|
1423
|
+
if (expected.startsWith("*.")) {
|
|
1424
|
+
const suffix = expected.slice(1);
|
|
1425
|
+
return host.endsWith(suffix) && host.length > suffix.length;
|
|
1426
|
+
}
|
|
1427
|
+
return host === expected;
|
|
1428
|
+
}
|
|
1429
|
+
function isLocal(hostname) {
|
|
1430
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
|
|
1431
|
+
}
|
|
1432
|
+
var LicenseManager = class {
|
|
1433
|
+
constructor(config = {}) {
|
|
1434
|
+
this.config = config;
|
|
1435
|
+
const hostname = config.hostname ?? globalThis.location?.hostname ?? "localhost";
|
|
1436
|
+
const environment = config.environment ?? "development";
|
|
1437
|
+
if (environment !== "production" || isLocal(hostname)) {
|
|
1438
|
+
this.status = { state: "exempt", payload: null };
|
|
1439
|
+
this.readyPromise = Promise.resolve(this.status);
|
|
1440
|
+
} else if (!config.key) {
|
|
1441
|
+
this.status = { state: "community", payload: null };
|
|
1442
|
+
this.readyPromise = Promise.resolve(this.status);
|
|
1443
|
+
} else {
|
|
1444
|
+
this.status = { state: "checking", payload: null };
|
|
1445
|
+
this.readyPromise = this.validate(config.key, hostname);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
config;
|
|
1449
|
+
status;
|
|
1450
|
+
readyPromise;
|
|
1451
|
+
getStatus() {
|
|
1452
|
+
return this.status;
|
|
1453
|
+
}
|
|
1454
|
+
ready() {
|
|
1455
|
+
return this.readyPromise;
|
|
1456
|
+
}
|
|
1457
|
+
hasFeature(feature) {
|
|
1458
|
+
return this.status.state === "valid" && (this.status.payload.features ?? []).includes(feature);
|
|
1459
|
+
}
|
|
1460
|
+
track(name) {
|
|
1461
|
+
this.config.onUsage?.({
|
|
1462
|
+
name,
|
|
1463
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1464
|
+
licenseId: this.status.state === "valid" ? this.status.payload.id : void 0
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
async validate(key, hostname) {
|
|
1468
|
+
const payload = await this.config.verifyOffline?.(key) ?? null;
|
|
1469
|
+
if (!payload) return this.status = { state: "invalid", payload: null };
|
|
1470
|
+
if (payload.expiresAt && Date.parse(payload.expiresAt) < Date.now()) {
|
|
1471
|
+
return this.status = { state: "expired", payload };
|
|
1472
|
+
}
|
|
1473
|
+
if (!payload.domains.some((domain) => domainMatches(hostname, domain))) {
|
|
1474
|
+
return this.status = { state: "domain-mismatch", payload };
|
|
1475
|
+
}
|
|
1476
|
+
return this.status = { state: "valid", payload };
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/project.ts
|
|
1481
|
+
var ProjectManager = class {
|
|
1482
|
+
constructor(editor) {
|
|
1483
|
+
this.editor = editor;
|
|
1484
|
+
const first = { id: generateId(), name: "Page 1", state: editor.toJSON() };
|
|
1485
|
+
this.pages = [first];
|
|
1486
|
+
this.activePageId = first.id;
|
|
1487
|
+
}
|
|
1488
|
+
editor;
|
|
1489
|
+
pages;
|
|
1490
|
+
activePageId;
|
|
1491
|
+
getAll() {
|
|
1492
|
+
return this.pages.map(({ id, name }) => ({ id, name }));
|
|
1493
|
+
}
|
|
1494
|
+
getActivePageId() {
|
|
1495
|
+
return this.activePageId;
|
|
1496
|
+
}
|
|
1497
|
+
add(name = `Page ${this.pages.length + 1}`, cloneCurrent = false) {
|
|
1498
|
+
this.saveCurrent();
|
|
1499
|
+
const source = structuredClone(this.editor.toJSON());
|
|
1500
|
+
const state = cloneCurrent ? source : this.blankState(source);
|
|
1501
|
+
const page = { id: generateId(), name, state };
|
|
1502
|
+
this.pages.push(page);
|
|
1503
|
+
this.emitChanged();
|
|
1504
|
+
return page.id;
|
|
1505
|
+
}
|
|
1506
|
+
async switchTo(id) {
|
|
1507
|
+
if (id === this.activePageId) return true;
|
|
1508
|
+
const page = this.pages.find((candidate) => candidate.id === id);
|
|
1509
|
+
if (!page) return false;
|
|
1510
|
+
this.saveCurrent();
|
|
1511
|
+
await this.editor.fromJSON(structuredClone(page.state));
|
|
1512
|
+
this.activePageId = id;
|
|
1513
|
+
this.emitChanged();
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
async duplicate(id) {
|
|
1517
|
+
this.saveCurrent();
|
|
1518
|
+
const source = this.pages.find((page2) => page2.id === id);
|
|
1519
|
+
if (!source) return null;
|
|
1520
|
+
const page = {
|
|
1521
|
+
id: generateId(),
|
|
1522
|
+
name: `${source.name} copy`,
|
|
1523
|
+
state: structuredClone(source.state)
|
|
1524
|
+
};
|
|
1525
|
+
const index = this.pages.indexOf(source);
|
|
1526
|
+
this.pages.splice(index + 1, 0, page);
|
|
1527
|
+
this.emitChanged();
|
|
1528
|
+
return page.id;
|
|
1529
|
+
}
|
|
1530
|
+
async remove(id) {
|
|
1531
|
+
if (this.pages.length === 1) return false;
|
|
1532
|
+
const index = this.pages.findIndex((page) => page.id === id);
|
|
1533
|
+
if (index < 0) return false;
|
|
1534
|
+
if (id === this.activePageId) {
|
|
1535
|
+
const next = this.pages[index + 1] ?? this.pages[index - 1];
|
|
1536
|
+
await this.editor.fromJSON(structuredClone(next.state));
|
|
1537
|
+
this.activePageId = next.id;
|
|
1538
|
+
}
|
|
1539
|
+
this.pages.splice(index, 1);
|
|
1540
|
+
this.emitChanged();
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
rename(id, name) {
|
|
1544
|
+
const page = this.pages.find((candidate) => candidate.id === id);
|
|
1545
|
+
const trimmed = name.trim();
|
|
1546
|
+
if (!page || !trimmed || page.name === trimmed) return false;
|
|
1547
|
+
page.name = trimmed;
|
|
1548
|
+
this.emitChanged();
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
reorder(id, newIndex) {
|
|
1552
|
+
const index = this.pages.findIndex((page2) => page2.id === id);
|
|
1553
|
+
if (index < 0 || !Number.isFinite(newIndex)) return false;
|
|
1554
|
+
const target = Math.max(0, Math.min(this.pages.length - 1, Math.round(newIndex)));
|
|
1555
|
+
if (target === index) return false;
|
|
1556
|
+
const [page] = this.pages.splice(index, 1);
|
|
1557
|
+
this.pages.splice(target, 0, page);
|
|
1558
|
+
this.emitChanged();
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
toJSON() {
|
|
1562
|
+
this.saveCurrent();
|
|
1563
|
+
return {
|
|
1564
|
+
version: "1.0.0",
|
|
1565
|
+
activePageId: this.activePageId,
|
|
1566
|
+
pages: structuredClone(this.pages)
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
async fromJSON(project) {
|
|
1570
|
+
if (project?.version !== "1.0.0" || !Array.isArray(project.pages) || project.pages.length === 0 || !project.pages.some((page) => page.id === project.activePageId)) {
|
|
1571
|
+
throw new Error("Invalid project state");
|
|
1572
|
+
}
|
|
1573
|
+
const pages = structuredClone(project.pages);
|
|
1574
|
+
const active = pages.find((page) => page.id === project.activePageId);
|
|
1575
|
+
await this.editor.fromJSON(structuredClone(active.state));
|
|
1576
|
+
this.pages = pages;
|
|
1577
|
+
this.activePageId = active.id;
|
|
1578
|
+
this.emitChanged();
|
|
1579
|
+
}
|
|
1580
|
+
saveCurrent() {
|
|
1581
|
+
const page = this.pages.find((candidate) => candidate.id === this.activePageId);
|
|
1582
|
+
if (page) page.state = structuredClone(this.editor.toJSON());
|
|
1583
|
+
}
|
|
1584
|
+
blankState(source) {
|
|
1585
|
+
return { ...source, layers: [], mockup: null };
|
|
1586
|
+
}
|
|
1587
|
+
emitChanged() {
|
|
1588
|
+
this.editor.events.emit("project:changed", {
|
|
1589
|
+
activePageId: this.activePageId,
|
|
1590
|
+
pages: this.getAll()
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1018
1595
|
// src/editor.ts
|
|
1019
1596
|
var MIN_ZOOM = 0.1;
|
|
1020
1597
|
var MAX_ZOOM = 8;
|
|
@@ -1027,6 +1604,9 @@ var CanvasEditor = class {
|
|
|
1027
1604
|
snapping;
|
|
1028
1605
|
crop;
|
|
1029
1606
|
patterns;
|
|
1607
|
+
fonts;
|
|
1608
|
+
licensing;
|
|
1609
|
+
pages;
|
|
1030
1610
|
fileAdapter;
|
|
1031
1611
|
imageProvider;
|
|
1032
1612
|
zoomLevel = 1;
|
|
@@ -1035,8 +1615,12 @@ var CanvasEditor = class {
|
|
|
1035
1615
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
1036
1616
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
1037
1617
|
designBackground;
|
|
1618
|
+
designBackgroundImage = null;
|
|
1038
1619
|
constructor(canvasElement, config) {
|
|
1039
1620
|
this.events = new EventEmitter();
|
|
1621
|
+
this.fonts = new FontRegistry();
|
|
1622
|
+
config.fonts?.forEach((font) => this.fonts.register(font));
|
|
1623
|
+
this.licensing = new LicenseManager(config.license);
|
|
1040
1624
|
this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
|
|
1041
1625
|
const widthPx = this.units.toPixels(config.width);
|
|
1042
1626
|
const heightPx = this.units.toPixels(config.height);
|
|
@@ -1059,18 +1643,73 @@ var CanvasEditor = class {
|
|
|
1059
1643
|
},
|
|
1060
1644
|
events: this.events
|
|
1061
1645
|
});
|
|
1646
|
+
this.layers.setHistoryCallback(() => this.history.save());
|
|
1062
1647
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
1063
1648
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
1064
|
-
this.patterns = new PatternManager(
|
|
1649
|
+
this.patterns = new PatternManager(
|
|
1650
|
+
this.canvas,
|
|
1651
|
+
this.layers,
|
|
1652
|
+
this.history,
|
|
1653
|
+
this.events,
|
|
1654
|
+
config.patternSourceResolver
|
|
1655
|
+
);
|
|
1065
1656
|
this.setupCanvasEvents();
|
|
1066
1657
|
this.history.saveImmediate();
|
|
1658
|
+
this.pages = new ProjectManager(this);
|
|
1067
1659
|
}
|
|
1068
1660
|
// ─── Layer Operations ────────────────────────────────
|
|
1069
1661
|
async addImage(url, options) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1662
|
+
try {
|
|
1663
|
+
const img = await import_fabric4.FabricImage.fromURL(
|
|
1664
|
+
url,
|
|
1665
|
+
{},
|
|
1666
|
+
{ originX: "left", originY: "top", ...options }
|
|
1667
|
+
);
|
|
1668
|
+
const layer = this.layers.add("image", img);
|
|
1669
|
+
this.history.save();
|
|
1670
|
+
return layer;
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
this.events.emit("error", { message: "Failed to add image", error });
|
|
1673
|
+
throw error;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
/** Replace an image source without changing its layer identity or visual transform. */
|
|
1677
|
+
async replaceImageSource(layerId, url) {
|
|
1678
|
+
const layer = this.layers.get(layerId);
|
|
1679
|
+
if (!layer || layer.type !== "image") throw new Error(`Image layer not found: ${layerId}`);
|
|
1680
|
+
if (layer.meta.pattern) {
|
|
1681
|
+
throw new Error("Clear the pattern before replacing the image source");
|
|
1682
|
+
}
|
|
1683
|
+
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
1684
|
+
const previous = layer.fabricObject;
|
|
1685
|
+
try {
|
|
1686
|
+
const replacement = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
1687
|
+
replacement.set({
|
|
1688
|
+
left: previous.left,
|
|
1689
|
+
top: previous.top,
|
|
1690
|
+
originX: previous.originX,
|
|
1691
|
+
originY: previous.originY,
|
|
1692
|
+
width: previous.width,
|
|
1693
|
+
height: previous.height,
|
|
1694
|
+
cropX: previous.cropX,
|
|
1695
|
+
cropY: previous.cropY,
|
|
1696
|
+
scaleX: previous.scaleX,
|
|
1697
|
+
scaleY: previous.scaleY,
|
|
1698
|
+
angle: previous.angle,
|
|
1699
|
+
skewX: previous.skewX,
|
|
1700
|
+
skewY: previous.skewY,
|
|
1701
|
+
flipX: previous.flipX,
|
|
1702
|
+
flipY: previous.flipY
|
|
1703
|
+
});
|
|
1704
|
+
replacement.filters = [...previous.filters];
|
|
1705
|
+
replacement.applyFilters();
|
|
1706
|
+
replacement.setCoords();
|
|
1707
|
+
this.layers.replaceObject(layerId, replacement);
|
|
1708
|
+
return layer;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
this.events.emit("error", { message: "Failed to replace image source", error });
|
|
1711
|
+
throw error;
|
|
1712
|
+
}
|
|
1074
1713
|
}
|
|
1075
1714
|
addText(text, options) {
|
|
1076
1715
|
const textbox = new import_fabric4.Textbox(text, {
|
|
@@ -1093,27 +1732,42 @@ var CanvasEditor = class {
|
|
|
1093
1732
|
this.history.save();
|
|
1094
1733
|
return layer;
|
|
1095
1734
|
}
|
|
1096
|
-
async addTemplate(template,
|
|
1097
|
-
const
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1735
|
+
async addTemplate(template, params) {
|
|
1736
|
+
const values = {};
|
|
1737
|
+
for (const parameter of template.parameters) {
|
|
1738
|
+
const value = params[parameter.key] ?? parameter.default;
|
|
1739
|
+
if (value === void 0) throw new Error(`Missing template parameter: ${parameter.key}`);
|
|
1740
|
+
if (parameter.type === "number" && !Number.isFinite(Number(value))) {
|
|
1741
|
+
throw new Error(`Invalid number for template parameter: ${parameter.key}`);
|
|
1742
|
+
}
|
|
1743
|
+
if (parameter.type === "color" && !isCssColor(value)) {
|
|
1744
|
+
throw new Error(`Invalid color for template parameter: ${parameter.key}`);
|
|
1745
|
+
}
|
|
1746
|
+
values[parameter.key] = value;
|
|
1747
|
+
}
|
|
1748
|
+
const resolved = sanitizeSvg(
|
|
1749
|
+
template.svg.replace(/\{\{(\w+)(?:[|:]([^}]*))?\}\}/g, (token, key, fallback) => {
|
|
1750
|
+
const value = values[key] ?? fallback;
|
|
1751
|
+
return value === void 0 ? token : escapeXml(value);
|
|
1752
|
+
})
|
|
1110
1753
|
);
|
|
1754
|
+
const { objects, options } = await (0, import_fabric4.loadSVGFromString)(resolved);
|
|
1755
|
+
const validObjects = objects.filter((object) => object !== null);
|
|
1756
|
+
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
1757
|
+
const group = import_fabric4.util.groupSVGElements(validObjects, options);
|
|
1758
|
+
group.set({
|
|
1759
|
+
left: this.canvas.getWidth() / 2,
|
|
1760
|
+
top: this.canvas.getHeight() / 2,
|
|
1761
|
+
originX: "center",
|
|
1762
|
+
originY: "center"
|
|
1763
|
+
});
|
|
1764
|
+
const layer = this.layers.add("template", group, template.name);
|
|
1111
1765
|
this.history.save();
|
|
1112
1766
|
return layer;
|
|
1113
1767
|
}
|
|
1114
1768
|
removeLayer(id) {
|
|
1115
|
-
this.
|
|
1116
|
-
this.history.save();
|
|
1769
|
+
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
1770
|
+
if (this.layers.remove(id)) this.history.save();
|
|
1117
1771
|
}
|
|
1118
1772
|
selectLayer(id) {
|
|
1119
1773
|
this.layers.select(id);
|
|
@@ -1148,35 +1802,173 @@ var CanvasEditor = class {
|
|
|
1148
1802
|
clone.setCoords();
|
|
1149
1803
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
1150
1804
|
copy.meta = structuredClone(layer.meta);
|
|
1805
|
+
copy.visible = layer.visible;
|
|
1806
|
+
copy.locked = layer.locked;
|
|
1807
|
+
copy.opacity = layer.opacity;
|
|
1808
|
+
clone.set({
|
|
1809
|
+
visible: layer.visible,
|
|
1810
|
+
selectable: !layer.locked,
|
|
1811
|
+
evented: !layer.locked,
|
|
1812
|
+
opacity: layer.opacity
|
|
1813
|
+
});
|
|
1151
1814
|
this.canvas.setActiveObject(clone);
|
|
1152
1815
|
this.canvas.requestRenderAll();
|
|
1153
1816
|
this.history.save();
|
|
1154
1817
|
return copy;
|
|
1155
1818
|
}
|
|
1819
|
+
applyImageAdjustments(layerId, adjustments) {
|
|
1820
|
+
const layer = this.layers.get(layerId);
|
|
1821
|
+
if (!layer || layer.type !== "image") return false;
|
|
1822
|
+
const image = layer.fabricObject;
|
|
1823
|
+
const previous = layer.meta.imageAdjustments ?? {};
|
|
1824
|
+
const next = { ...previous, ...adjustments };
|
|
1825
|
+
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
1826
|
+
image.filters = [
|
|
1827
|
+
new import_fabric4.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
1828
|
+
new import_fabric4.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
1829
|
+
new import_fabric4.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
1830
|
+
new import_fabric4.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
1831
|
+
];
|
|
1832
|
+
layer.meta.imageAdjustments = next;
|
|
1833
|
+
image.applyFilters();
|
|
1834
|
+
this.canvas.requestRenderAll();
|
|
1835
|
+
this.history.save();
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
/** Combine two or more layers into a single editable group layer. */
|
|
1839
|
+
async groupLayers(ids, name = "Group") {
|
|
1840
|
+
const uniqueIds = [...new Set(ids)];
|
|
1841
|
+
const children = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0);
|
|
1842
|
+
if (children.length < 2 || children.length !== uniqueIds.length) return null;
|
|
1843
|
+
return this.history.transaction(() => {
|
|
1844
|
+
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
1845
|
+
const objects = children.map((layer) => layer.fabricObject);
|
|
1846
|
+
for (const layer of children) this.layers.remove(layer.id);
|
|
1847
|
+
const group = new import_fabric4.Group(objects);
|
|
1848
|
+
const grouped = this.layers.add("group", group, name);
|
|
1849
|
+
grouped.meta.groupChildren = childData;
|
|
1850
|
+
this.layers.select(grouped.id);
|
|
1851
|
+
this.history.save();
|
|
1852
|
+
return grouped;
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
/** Restore a group created by groupLayers back to its original layer records. */
|
|
1856
|
+
async ungroupLayer(id) {
|
|
1857
|
+
const grouped = this.layers.get(id);
|
|
1858
|
+
const childData = grouped?.meta.groupChildren;
|
|
1859
|
+
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
1860
|
+
const group = grouped.fabricObject;
|
|
1861
|
+
return this.history.transaction(() => {
|
|
1862
|
+
const transform = group.calcTransformMatrix();
|
|
1863
|
+
const objects = group.removeAll();
|
|
1864
|
+
this.layers.remove(id);
|
|
1865
|
+
const restored = objects.map((object, index) => {
|
|
1866
|
+
import_fabric4.util.addTransformToObject(object, transform);
|
|
1867
|
+
object.setCoords();
|
|
1868
|
+
const data = childData[index];
|
|
1869
|
+
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
1870
|
+
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
1871
|
+
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
1872
|
+
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
1873
|
+
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
1874
|
+
return layer;
|
|
1875
|
+
});
|
|
1876
|
+
this.history.save();
|
|
1877
|
+
this.layers.select(restored[0]?.id ?? null);
|
|
1878
|
+
return restored;
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1156
1881
|
// ─── Serialization ──────────────────────────────────
|
|
1157
1882
|
toJSON() {
|
|
1158
1883
|
return serializeEditor(this);
|
|
1159
1884
|
}
|
|
1160
1885
|
async fromJSON(state) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1886
|
+
const managedByHistory = this.history.isRestoring();
|
|
1887
|
+
if (!managedByHistory) {
|
|
1888
|
+
this.history.saveImmediate();
|
|
1889
|
+
this.history.pause();
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
await deserializeEditor(this, state);
|
|
1893
|
+
this.patterns.repinAll();
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
if (!managedByHistory) {
|
|
1896
|
+
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
1897
|
+
}
|
|
1898
|
+
throw error;
|
|
1899
|
+
} finally {
|
|
1900
|
+
if (!managedByHistory) this.history.resume();
|
|
1901
|
+
}
|
|
1902
|
+
if (!managedByHistory) {
|
|
1903
|
+
this.history.clear();
|
|
1904
|
+
this.history.saveImmediate();
|
|
1905
|
+
}
|
|
1163
1906
|
}
|
|
1164
1907
|
// ─── Export ──────────────────────────────────────────
|
|
1165
1908
|
async toPNG(options) {
|
|
1166
|
-
this.
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
return
|
|
1909
|
+
return this.toRaster(options?.format ?? "png", options);
|
|
1910
|
+
}
|
|
1911
|
+
async toJPEG(options) {
|
|
1912
|
+
return this.toRaster("jpeg", options);
|
|
1913
|
+
}
|
|
1914
|
+
async toWebP(options) {
|
|
1915
|
+
return this.toRaster("webp", options);
|
|
1916
|
+
}
|
|
1917
|
+
async toRaster(format, options) {
|
|
1918
|
+
this.events.emit("export:start", { format });
|
|
1919
|
+
try {
|
|
1920
|
+
await this.fonts.ready();
|
|
1921
|
+
const blob = await this.withDesignBackground(
|
|
1922
|
+
() => exportPNG(this.canvas, { ...options, format })
|
|
1923
|
+
);
|
|
1924
|
+
this.events.emit("export:complete", { format });
|
|
1925
|
+
this.licensing.track(`export:${format}`);
|
|
1926
|
+
return blob;
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
this.events.emit("error", { message: `Failed to export ${format.toUpperCase()}`, error });
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1170
1931
|
}
|
|
1171
1932
|
toSVG() {
|
|
1172
1933
|
this.events.emit("export:start", { format: "svg" });
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1934
|
+
try {
|
|
1935
|
+
const svg = this.withDesignBackground(() => exportSVG(this.canvas));
|
|
1936
|
+
this.events.emit("export:complete", { format: "svg" });
|
|
1937
|
+
this.licensing.track("export:svg");
|
|
1938
|
+
return svg;
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
this.events.emit("error", { message: "Failed to export SVG", error });
|
|
1941
|
+
throw error;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async toSVGAsync(options = {}) {
|
|
1945
|
+
await this.fonts.ready();
|
|
1946
|
+
const svg = this.toSVG();
|
|
1947
|
+
if (options.embedFonts === false || this.fonts.getAll().length === 0) return svg;
|
|
1948
|
+
try {
|
|
1949
|
+
const css = await this.fonts.getEmbeddedCss();
|
|
1950
|
+
return svg.replace(/(<svg\b[^>]*>)/i, `$1<defs><style>${css}</style></defs>`);
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
this.events.emit("error", { message: "Failed to embed fonts in SVG", error });
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1176
1955
|
}
|
|
1177
1956
|
toDataURL(format = "png", multiplier = 1) {
|
|
1178
1957
|
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
1179
1958
|
}
|
|
1959
|
+
/** Export the current product-preview composite. Advanced warping is host-defined. */
|
|
1960
|
+
async toMockupImage(options = {}) {
|
|
1961
|
+
if (!this.mockup) throw new Error("No mockup is configured");
|
|
1962
|
+
await this.fonts.ready();
|
|
1963
|
+
try {
|
|
1964
|
+
const blob = await exportMockup(this.canvas, this.mockup, options);
|
|
1965
|
+
this.licensing.track(`export:mockup:${options.format ?? "png"}`);
|
|
1966
|
+
return blob;
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
this.events.emit("error", { message: "Failed to export mockup", error });
|
|
1969
|
+
throw error;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1180
1972
|
/**
|
|
1181
1973
|
* Run an export with the configured design background applied, even when a
|
|
1182
1974
|
* mockup preview has forced the live canvas transparent — so exports reflect
|
|
@@ -1185,11 +1977,14 @@ var CanvasEditor = class {
|
|
|
1185
1977
|
withDesignBackground(fn) {
|
|
1186
1978
|
if (!this.mockup) return fn();
|
|
1187
1979
|
const previewBg = this.canvas.backgroundColor;
|
|
1980
|
+
const previewImage = this.canvas.backgroundImage;
|
|
1188
1981
|
this.canvas.backgroundColor = this.designBackground;
|
|
1982
|
+
this.canvas.backgroundImage = this.designBackgroundImage ?? void 0;
|
|
1189
1983
|
try {
|
|
1190
1984
|
return fn();
|
|
1191
1985
|
} finally {
|
|
1192
1986
|
this.canvas.backgroundColor = previewBg;
|
|
1987
|
+
this.canvas.backgroundImage = previewImage;
|
|
1193
1988
|
}
|
|
1194
1989
|
}
|
|
1195
1990
|
toPrintifyPositioning() {
|
|
@@ -1204,7 +1999,8 @@ var CanvasEditor = class {
|
|
|
1204
1999
|
for (const layer of imageLayers) {
|
|
1205
2000
|
const obj = layer.fabricObject;
|
|
1206
2001
|
const center = obj.getCenterPoint();
|
|
1207
|
-
result[layer.name]
|
|
2002
|
+
const key = result[layer.name] ? `${layer.name}-${layer.id}` : layer.name;
|
|
2003
|
+
result[key] = {
|
|
1208
2004
|
x: round2(center.x / canvasW),
|
|
1209
2005
|
y: round2(center.y / canvasH),
|
|
1210
2006
|
scale: round2(obj.scaleX ?? 1),
|
|
@@ -1213,26 +2009,92 @@ var CanvasEditor = class {
|
|
|
1213
2009
|
}
|
|
1214
2010
|
return result;
|
|
1215
2011
|
}
|
|
2012
|
+
/** Provider-neutral, ID-stable placement data in 0..1 canvas coordinates. */
|
|
2013
|
+
toNormalizedPositioning() {
|
|
2014
|
+
const canvasWidth = this.canvas.getWidth();
|
|
2015
|
+
const canvasHeight = this.canvas.getHeight();
|
|
2016
|
+
return this.layers.getAll().map((layer) => {
|
|
2017
|
+
const object = layer.fabricObject;
|
|
2018
|
+
const center = object.getCenterPoint();
|
|
2019
|
+
const bounds = object.getBoundingRect();
|
|
2020
|
+
return {
|
|
2021
|
+
layerId: layer.id,
|
|
2022
|
+
name: layer.name,
|
|
2023
|
+
type: layer.type,
|
|
2024
|
+
centerX: round2(center.x / canvasWidth),
|
|
2025
|
+
centerY: round2(center.y / canvasHeight),
|
|
2026
|
+
width: round2(bounds.width / canvasWidth),
|
|
2027
|
+
height: round2(bounds.height / canvasHeight),
|
|
2028
|
+
scaleX: round2(object.scaleX ?? 1),
|
|
2029
|
+
scaleY: round2(object.scaleY ?? 1),
|
|
2030
|
+
angle: round2(object.angle ?? 0)
|
|
2031
|
+
};
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
toProviderPositioning(adapter) {
|
|
2035
|
+
return adapter.map(this.toNormalizedPositioning(), {
|
|
2036
|
+
width: this.canvas.getWidth(),
|
|
2037
|
+
height: this.canvas.getHeight()
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
1216
2040
|
// ─── File Operations ────────────────────────────────
|
|
1217
2041
|
async save(filename, format) {
|
|
1218
2042
|
if (!this.fileAdapter) return void 0;
|
|
1219
2043
|
let data;
|
|
1220
2044
|
if (format === "png") {
|
|
1221
2045
|
data = await this.toPNG();
|
|
2046
|
+
} else if (format === "jpeg") {
|
|
2047
|
+
data = await this.toJPEG();
|
|
2048
|
+
} else if (format === "webp") {
|
|
2049
|
+
data = await this.toWebP();
|
|
1222
2050
|
} else if (format === "svg") {
|
|
1223
|
-
data = this.
|
|
2051
|
+
data = await this.toSVGAsync();
|
|
1224
2052
|
} else {
|
|
1225
2053
|
data = JSON.stringify(this.toJSON(), null, 2);
|
|
1226
2054
|
}
|
|
1227
|
-
|
|
2055
|
+
try {
|
|
2056
|
+
return await this.fileAdapter.save(data, filename, format);
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
this.events.emit("error", { message: `Failed to save ${format}`, error });
|
|
2059
|
+
throw error;
|
|
2060
|
+
}
|
|
1228
2061
|
}
|
|
1229
2062
|
async uploadImage(file) {
|
|
1230
|
-
if (!this.imageProvider) return void 0;
|
|
1231
|
-
|
|
2063
|
+
if (!this.imageProvider?.upload) return void 0;
|
|
2064
|
+
try {
|
|
2065
|
+
return await this.imageProvider.upload(file);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
this.events.emit("error", { message: "Failed to upload image", error });
|
|
2068
|
+
throw error;
|
|
2069
|
+
}
|
|
1232
2070
|
}
|
|
1233
2071
|
async browseImages() {
|
|
1234
2072
|
if (!this.imageProvider?.browse) return null;
|
|
1235
|
-
|
|
2073
|
+
try {
|
|
2074
|
+
return await this.imageProvider.browse();
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
this.events.emit("error", { message: "Failed to browse images", error });
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async searchImages(query, options) {
|
|
2081
|
+
if (!this.imageProvider?.search) return null;
|
|
2082
|
+
try {
|
|
2083
|
+
return await this.imageProvider.search(query, options);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
this.events.emit("error", { message: "Failed to search images", error });
|
|
2086
|
+
throw error;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
/** Track provider usage, then insert its hotlinked image as a normal layer. */
|
|
2090
|
+
async addProviderImage(image, options) {
|
|
2091
|
+
try {
|
|
2092
|
+
await this.imageProvider?.trackUse?.(image);
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
this.events.emit("error", { message: "Failed to record image use", error });
|
|
2095
|
+
throw error;
|
|
2096
|
+
}
|
|
2097
|
+
return this.addImage(image.url, options);
|
|
1236
2098
|
}
|
|
1237
2099
|
setFileAdapter(adapter) {
|
|
1238
2100
|
this.fileAdapter = adapter;
|
|
@@ -1248,15 +2110,130 @@ var CanvasEditor = class {
|
|
|
1248
2110
|
this.canvas.requestRenderAll();
|
|
1249
2111
|
}
|
|
1250
2112
|
this.history.save();
|
|
2113
|
+
this.events.emit("canvas:modified", {});
|
|
1251
2114
|
}
|
|
1252
2115
|
getDesignBackground() {
|
|
1253
2116
|
return this.designBackground;
|
|
1254
2117
|
}
|
|
1255
|
-
|
|
2118
|
+
getDesignBackgroundImage() {
|
|
2119
|
+
return this.designBackgroundImage;
|
|
2120
|
+
}
|
|
2121
|
+
async setBackgroundImage(url, options = {}) {
|
|
2122
|
+
if (url === null) {
|
|
2123
|
+
this.setBackgroundImageObject(null);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
try {
|
|
2127
|
+
const image = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2128
|
+
const width = image.width || 1;
|
|
2129
|
+
const height = image.height || 1;
|
|
2130
|
+
const canvasWidth = this.canvas.getWidth();
|
|
2131
|
+
const canvasHeight = this.canvas.getHeight();
|
|
2132
|
+
const fit = options.fit ?? "cover";
|
|
2133
|
+
const sx = canvasWidth / width;
|
|
2134
|
+
const sy = canvasHeight / height;
|
|
2135
|
+
const scaleX = fit === "stretch" ? sx : fit === "contain" ? Math.min(sx, sy) : Math.max(sx, sy);
|
|
2136
|
+
const scaleY = fit === "stretch" ? sy : scaleX;
|
|
2137
|
+
image.set({
|
|
2138
|
+
left: (canvasWidth - width * scaleX) / 2,
|
|
2139
|
+
top: (canvasHeight - height * scaleY) / 2,
|
|
2140
|
+
scaleX,
|
|
2141
|
+
scaleY,
|
|
2142
|
+
opacity: clamp(options.opacity ?? 1, 0, 1),
|
|
2143
|
+
selectable: false,
|
|
2144
|
+
evented: false
|
|
2145
|
+
});
|
|
2146
|
+
this.setBackgroundImageObject(image);
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
this.events.emit("error", { message: "Failed to set background image", error });
|
|
2149
|
+
throw error;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/** Used by state restoration and advanced integrations with an existing Fabric object. */
|
|
2153
|
+
setBackgroundImageObject(image, save = true) {
|
|
2154
|
+
this.designBackgroundImage = image;
|
|
2155
|
+
this.canvas.backgroundImage = this.mockup ? void 0 : image ?? void 0;
|
|
2156
|
+
this.canvas.requestRenderAll();
|
|
2157
|
+
if (save) this.history.save();
|
|
2158
|
+
this.events.emit("canvas:modified", {});
|
|
2159
|
+
}
|
|
2160
|
+
setTransparentBackground() {
|
|
2161
|
+
this.setBackground("");
|
|
2162
|
+
}
|
|
2163
|
+
resize(width, height, options = {}) {
|
|
1256
2164
|
const widthPx = this.units.toPixels(width);
|
|
1257
2165
|
const heightPx = this.units.toPixels(height);
|
|
2166
|
+
const oldWidth = this.canvas.getWidth();
|
|
2167
|
+
const oldHeight = this.canvas.getHeight();
|
|
2168
|
+
const scaleContent = options.scaleContent ?? true;
|
|
2169
|
+
if (scaleContent && oldWidth > 0 && oldHeight > 0) {
|
|
2170
|
+
const sx = widthPx / oldWidth;
|
|
2171
|
+
const sy = heightPx / oldHeight;
|
|
2172
|
+
for (const layer of this.layers.getAll()) {
|
|
2173
|
+
if (layer.meta.pattern) continue;
|
|
2174
|
+
const object = layer.fabricObject;
|
|
2175
|
+
object.set({
|
|
2176
|
+
left: (object.left ?? 0) * sx,
|
|
2177
|
+
top: (object.top ?? 0) * sy,
|
|
2178
|
+
scaleX: (object.scaleX ?? 1) * sx,
|
|
2179
|
+
scaleY: (object.scaleY ?? 1) * sy
|
|
2180
|
+
});
|
|
2181
|
+
object.setCoords();
|
|
2182
|
+
}
|
|
2183
|
+
if (this.designBackgroundImage) {
|
|
2184
|
+
this.designBackgroundImage.set({
|
|
2185
|
+
left: (this.designBackgroundImage.left ?? 0) * sx,
|
|
2186
|
+
top: (this.designBackgroundImage.top ?? 0) * sy,
|
|
2187
|
+
scaleX: (this.designBackgroundImage.scaleX ?? 1) * sx,
|
|
2188
|
+
scaleY: (this.designBackgroundImage.scaleY ?? 1) * sy
|
|
2189
|
+
});
|
|
2190
|
+
this.designBackgroundImage.setCoords();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
1258
2193
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
2194
|
+
this.patterns.repinAll();
|
|
1259
2195
|
this.canvas.requestRenderAll();
|
|
2196
|
+
this.history.save();
|
|
2197
|
+
this.events.emit("canvas:modified", {});
|
|
2198
|
+
}
|
|
2199
|
+
/** Effective source resolution for an image at its current physical size. */
|
|
2200
|
+
getImageDpi(layerId) {
|
|
2201
|
+
const layer = this.layers.get(layerId);
|
|
2202
|
+
if (!layer || layer.type !== "image") return null;
|
|
2203
|
+
const image = layer.fabricObject;
|
|
2204
|
+
const sourcePixels = image.width ?? 0;
|
|
2205
|
+
const displayedPixels = image.getScaledWidth();
|
|
2206
|
+
if (sourcePixels <= 0 || displayedPixels <= 0) return null;
|
|
2207
|
+
return round2(sourcePixels / (displayedPixels / this.units.getDpi()));
|
|
2208
|
+
}
|
|
2209
|
+
validateImageDpi(minimumDpi = 300) {
|
|
2210
|
+
return this.layers.getAll().filter((layer) => layer.type === "image" && !layer.meta.pattern).flatMap((layer) => {
|
|
2211
|
+
const effectiveDpi = this.getImageDpi(layer.id);
|
|
2212
|
+
return effectiveDpi !== null && effectiveDpi < minimumDpi ? [{ layerId: layer.id, layerName: layer.name, effectiveDpi, minimumDpi }] : [];
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
/** Render a small data-URL preview without changing document dimensions. */
|
|
2216
|
+
toThumbnail(maxWidth = 320, maxHeight = 320, format = "png") {
|
|
2217
|
+
const multiplier = Math.min(
|
|
2218
|
+
1,
|
|
2219
|
+
maxWidth / this.canvas.getWidth(),
|
|
2220
|
+
maxHeight / this.canvas.getHeight()
|
|
2221
|
+
);
|
|
2222
|
+
return this.toDataURL(format, multiplier);
|
|
2223
|
+
}
|
|
2224
|
+
bringToFront(id) {
|
|
2225
|
+
return this.layers.reorder(id, this.layers.count() - 1);
|
|
2226
|
+
}
|
|
2227
|
+
sendToBack(id) {
|
|
2228
|
+
return this.layers.reorder(id, 0);
|
|
2229
|
+
}
|
|
2230
|
+
bringForward(id) {
|
|
2231
|
+
const index = this.layers.getAll().findIndex((layer) => layer.id === id);
|
|
2232
|
+
return index >= 0 && this.layers.reorder(id, index + 1);
|
|
2233
|
+
}
|
|
2234
|
+
sendBackward(id) {
|
|
2235
|
+
const index = this.layers.getAll().findIndex((layer) => layer.id === id);
|
|
2236
|
+
return index >= 0 && this.layers.reorder(id, index - 1);
|
|
1260
2237
|
}
|
|
1261
2238
|
// ─── Zoom ───────────────────────────────────────────
|
|
1262
2239
|
//
|
|
@@ -1294,6 +2271,20 @@ var CanvasEditor = class {
|
|
|
1294
2271
|
const sy = (viewportHeight - padding * 2) / h;
|
|
1295
2272
|
this.setZoom(Math.min(sx, sy));
|
|
1296
2273
|
}
|
|
2274
|
+
/** Zoom until the current selection fills the artboard viewport. */
|
|
2275
|
+
zoomToSelection(padding = 24) {
|
|
2276
|
+
const active = this.canvas.getActiveObject();
|
|
2277
|
+
if (!active) return;
|
|
2278
|
+
active.setCoords();
|
|
2279
|
+
const bounds = active.getBoundingRect();
|
|
2280
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
2281
|
+
this.setZoom(
|
|
2282
|
+
Math.min(
|
|
2283
|
+
(this.canvas.getWidth() - padding * 2) / bounds.width,
|
|
2284
|
+
(this.canvas.getHeight() - padding * 2) / bounds.height
|
|
2285
|
+
)
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
1297
2288
|
// ─── Patterns ───────────────────────────────────────
|
|
1298
2289
|
applyPattern(layerId, config) {
|
|
1299
2290
|
return this.patterns.apply(layerId, config);
|
|
@@ -1305,8 +2296,10 @@ var CanvasEditor = class {
|
|
|
1305
2296
|
setMockup(mockup) {
|
|
1306
2297
|
this.mockup = mockup;
|
|
1307
2298
|
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
2299
|
+
this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
|
|
1308
2300
|
this.canvas.requestRenderAll();
|
|
1309
2301
|
this.events.emit("mockup:changed", { mockup });
|
|
2302
|
+
this.history.save();
|
|
1310
2303
|
}
|
|
1311
2304
|
clearMockup() {
|
|
1312
2305
|
this.setMockup(null);
|
|
@@ -1319,6 +2312,7 @@ var CanvasEditor = class {
|
|
|
1319
2312
|
this.snapping.dispose();
|
|
1320
2313
|
this.crop.dispose();
|
|
1321
2314
|
this.history.dispose();
|
|
2315
|
+
clearPatternImageCache();
|
|
1322
2316
|
this.events.removeAllListeners();
|
|
1323
2317
|
this.canvas.dispose();
|
|
1324
2318
|
}
|
|
@@ -1359,16 +2353,30 @@ var DEFAULT_PATTERN_CONFIG = {
|
|
|
1359
2353
|
rotationStepH: 0,
|
|
1360
2354
|
rotationStepV: 0
|
|
1361
2355
|
};
|
|
2356
|
+
|
|
2357
|
+
// src/presets.ts
|
|
2358
|
+
var CANVAS_SIZE_PRESETS = [
|
|
2359
|
+
{ id: "a4-portrait", name: "A4 portrait", width: 210, height: 297, unit: "mm", dpi: 300 },
|
|
2360
|
+
{ id: "a4-landscape", name: "A4 landscape", width: 297, height: 210, unit: "mm", dpi: 300 },
|
|
2361
|
+
{ id: "us-letter", name: "US Letter", width: 8.5, height: 11, unit: "in", dpi: 300 },
|
|
2362
|
+
{ id: "shirt-front", name: "Garment front", width: 12, height: 16, unit: "in", dpi: 300 },
|
|
2363
|
+
{ id: "instagram-square", name: "Social square", width: 1080, height: 1080, unit: "px", dpi: 72 },
|
|
2364
|
+
{ id: "story", name: "Story", width: 1080, height: 1920, unit: "px", dpi: 72 }
|
|
2365
|
+
];
|
|
1362
2366
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1363
2367
|
0 && (module.exports = {
|
|
2368
|
+
CANVAS_SIZE_PRESETS,
|
|
1364
2369
|
CanvasEditor,
|
|
1365
2370
|
CropController,
|
|
1366
2371
|
DEFAULT_PATTERN_CONFIG,
|
|
1367
2372
|
EventEmitter,
|
|
2373
|
+
FontRegistry,
|
|
1368
2374
|
HistoryManager,
|
|
1369
2375
|
Layer,
|
|
1370
2376
|
LayerManager,
|
|
2377
|
+
LicenseManager,
|
|
1371
2378
|
PatternManager,
|
|
2379
|
+
ProjectManager,
|
|
1372
2380
|
SnapManager,
|
|
1373
2381
|
UnitConverter,
|
|
1374
2382
|
applyPatternLocks,
|
|
@@ -1376,15 +2384,22 @@ var DEFAULT_PATTERN_CONFIG = {
|
|
|
1376
2384
|
captureLocks,
|
|
1377
2385
|
clamp,
|
|
1378
2386
|
clearPatternImageCache,
|
|
2387
|
+
computeCoverPlacement,
|
|
2388
|
+
computePrintAreaClip,
|
|
1379
2389
|
computeTilePositions,
|
|
1380
2390
|
deserializeEditor,
|
|
1381
2391
|
drawTiles,
|
|
2392
|
+
escapeXml,
|
|
1382
2393
|
exportDataURL,
|
|
2394
|
+
exportMockup,
|
|
1383
2395
|
exportPNG,
|
|
1384
2396
|
exportSVG,
|
|
1385
2397
|
generateId,
|
|
2398
|
+
isCssColor,
|
|
2399
|
+
loadPatternImage,
|
|
1386
2400
|
restoreLocks,
|
|
1387
2401
|
round2,
|
|
2402
|
+
sanitizeSvg,
|
|
1388
2403
|
serializeEditor
|
|
1389
2404
|
});
|
|
1390
2405
|
//# sourceMappingURL=index.js.map
|