@overtone-art/canvas-editor-core 0.2.6 → 0.2.8

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/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,26 @@ 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
+ AnnotationOverlay: () => AnnotationOverlay,
24
+ CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
33
25
  CanvasEditor: () => CanvasEditor,
34
26
  CropController: () => CropController,
35
27
  DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
36
28
  EventEmitter: () => EventEmitter,
29
+ FontRegistry: () => FontRegistry,
37
30
  HistoryManager: () => HistoryManager,
38
31
  Layer: () => Layer,
39
32
  LayerManager: () => LayerManager,
33
+ LicenseManager: () => LicenseManager,
34
+ MaskController: () => MaskController,
35
+ MaskRefinementError: () => MaskRefinementError,
40
36
  PatternManager: () => PatternManager,
37
+ ProjectManager: () => ProjectManager,
41
38
  SnapManager: () => SnapManager,
42
39
  UnitConverter: () => UnitConverter,
43
40
  applyPatternLocks: () => applyPatternLocks,
@@ -45,21 +42,29 @@ __export(index_exports, {
45
42
  captureLocks: () => captureLocks,
46
43
  clamp: () => clamp,
47
44
  clearPatternImageCache: () => clearPatternImageCache,
45
+ computeCoverPlacement: () => computeCoverPlacement,
46
+ computePrintAreaClip: () => computePrintAreaClip,
48
47
  computeTilePositions: () => computeTilePositions,
49
48
  deserializeEditor: () => deserializeEditor,
49
+ displaceRgba: () => displaceRgba,
50
50
  drawTiles: () => drawTiles,
51
+ escapeXml: () => escapeXml,
51
52
  exportDataURL: () => exportDataURL,
53
+ exportMockup: () => exportMockup,
52
54
  exportPNG: () => exportPNG,
53
55
  exportSVG: () => exportSVG,
54
56
  generateId: () => generateId,
57
+ isCssColor: () => isCssColor,
58
+ loadPatternImage: () => loadPatternImage,
55
59
  restoreLocks: () => restoreLocks,
56
60
  round2: () => round2,
61
+ sanitizeSvg: () => sanitizeSvg,
57
62
  serializeEditor: () => serializeEditor
58
63
  });
59
64
  module.exports = __toCommonJS(index_exports);
60
65
 
61
66
  // src/editor.ts
62
- var import_fabric4 = require("fabric");
67
+ var import_fabric6 = require("fabric");
63
68
 
64
69
  // src/events.ts
65
70
  var EventEmitter = class {
@@ -151,6 +156,10 @@ var LayerManager = class {
151
156
  canvas;
152
157
  events;
153
158
  layers = [];
159
+ onPropertyChanged;
160
+ setHistoryCallback(callback) {
161
+ this.onPropertyChanged = callback;
162
+ }
154
163
  add(type, fabricObject, name, id) {
155
164
  const layer = new Layer(type, fabricObject, name, id);
156
165
  this.layers.push(layer);
@@ -167,12 +176,39 @@ var LayerManager = class {
167
176
  this.layers.splice(index, 1);
168
177
  this.events.emit("layer:removed", { layerId: id });
169
178
  this.emitChanged();
179
+ this.onPropertyChanged?.();
180
+ return true;
181
+ }
182
+ /** Replace a layer's render object while preserving its immutable ID and panel state. */
183
+ replaceObject(id, fabricObject) {
184
+ const layer = this.get(id);
185
+ if (!layer || layer.fabricObject === fabricObject) return false;
186
+ const previous = layer.fabricObject;
187
+ const stackIndex = this.canvas.getObjects().indexOf(previous);
188
+ const wasActive = this.canvas.getActiveObject() === previous;
189
+ this.canvas.remove(previous);
190
+ layer.fabricObject = fabricObject;
191
+ fabricObject._layerId = id;
192
+ fabricObject.set({
193
+ visible: layer.visible,
194
+ opacity: layer.opacity,
195
+ selectable: !layer.locked,
196
+ evented: !layer.locked
197
+ });
198
+ this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
199
+ if (wasActive) this.canvas.setActiveObject(fabricObject);
200
+ this.canvas.requestRenderAll();
201
+ this.events.emit("layer:modified", { layerId: id });
202
+ this.emitChanged();
203
+ this.onPropertyChanged?.();
170
204
  return true;
171
205
  }
172
206
  reorder(id, newIndex) {
173
207
  const oldIndex = this.layers.findIndex((l) => l.id === id);
174
208
  if (oldIndex === -1) return false;
175
- const clamped = Math.max(0, Math.min(this.layers.length - 1, newIndex));
209
+ if (!Number.isFinite(newIndex)) return false;
210
+ const clamped = Math.max(0, Math.min(this.layers.length - 1, Math.round(newIndex)));
211
+ if (oldIndex === clamped) return false;
176
212
  const [layer] = this.layers.splice(oldIndex, 1);
177
213
  this.layers.splice(clamped, 0, layer);
178
214
  this.layers.forEach((l, i) => {
@@ -180,6 +216,7 @@ var LayerManager = class {
180
216
  });
181
217
  this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
182
218
  this.emitChanged();
219
+ this.onPropertyChanged?.();
183
220
  return true;
184
221
  }
185
222
  select(id) {
@@ -210,33 +247,42 @@ var LayerManager = class {
210
247
  setVisibility(id, visible) {
211
248
  const layer = this.get(id);
212
249
  if (!layer) return;
250
+ if (layer.visible === visible) return;
213
251
  layer.visible = visible;
214
252
  layer.fabricObject.visible = visible;
215
253
  this.canvas.requestRenderAll();
216
254
  this.emitChanged();
255
+ this.onPropertyChanged?.();
217
256
  }
218
257
  setLocked(id, locked) {
219
258
  const layer = this.get(id);
220
259
  if (!layer) return;
260
+ if (layer.locked === locked) return;
221
261
  layer.locked = locked;
222
262
  layer.fabricObject.selectable = !locked;
223
263
  layer.fabricObject.evented = !locked;
224
264
  this.canvas.requestRenderAll();
225
265
  this.emitChanged();
266
+ this.onPropertyChanged?.();
226
267
  }
227
268
  setOpacity(id, opacity) {
228
269
  const layer = this.get(id);
229
- if (!layer) return;
230
- layer.opacity = opacity;
231
- layer.fabricObject.opacity = opacity;
270
+ if (!layer || !Number.isFinite(opacity)) return;
271
+ const next = Math.max(0, Math.min(1, opacity));
272
+ if (layer.opacity === next) return;
273
+ layer.opacity = next;
274
+ layer.fabricObject.opacity = next;
232
275
  this.canvas.requestRenderAll();
233
276
  this.emitChanged();
277
+ this.onPropertyChanged?.();
234
278
  }
235
279
  setName(id, name) {
236
280
  const layer = this.get(id);
237
281
  if (!layer) return;
282
+ if (layer.name === name) return;
238
283
  layer.name = name;
239
284
  this.emitChanged();
285
+ this.onPropertyChanged?.();
240
286
  }
241
287
  clear() {
242
288
  for (const layer of this.layers) {
@@ -256,61 +302,133 @@ var LayerManager = class {
256
302
  };
257
303
 
258
304
  // src/history.ts
259
- var HistoryManager = class {
305
+ var HistoryManager = class _HistoryManager {
306
+ static ASSET_KEY = "__canvasEditorHistoryAsset";
260
307
  undoStack = [];
261
308
  redoStack = [];
309
+ assets = /* @__PURE__ */ new Map();
310
+ assetIds = /* @__PURE__ */ new Map();
311
+ nextAssetId = 1;
262
312
  maxSize;
313
+ maxBytes;
263
314
  paused = false;
264
315
  debounceTimer = null;
265
316
  debounceMs;
266
317
  getState;
267
318
  restoreState;
268
319
  events;
320
+ transactionDepth = 0;
321
+ transactionDirty = false;
269
322
  constructor(opts) {
270
323
  this.getState = opts.getState;
271
324
  this.restoreState = opts.restoreState;
272
325
  this.events = opts.events;
273
326
  this.maxSize = opts.maxSize ?? 50;
327
+ this.maxBytes = opts.maxBytes ?? 50 * 1024 * 1024;
274
328
  this.debounceMs = opts.debounceMs ?? 300;
275
329
  }
276
330
  save() {
277
331
  if (this.paused) return;
332
+ if (this.transactionDepth > 0) {
333
+ this.transactionDirty = true;
334
+ return;
335
+ }
278
336
  if (this.debounceTimer) {
279
337
  clearTimeout(this.debounceTimer);
280
338
  }
281
339
  this.debounceTimer = setTimeout(() => {
282
340
  this.saveImmediate();
283
341
  }, this.debounceMs);
342
+ this.emitChanged();
284
343
  }
285
344
  saveImmediate() {
286
- if (this.paused) return;
287
- const state = this.getState();
345
+ if (this.paused) {
346
+ this.cancelPending();
347
+ return;
348
+ }
349
+ if (this.transactionDepth > 0) {
350
+ this.transactionDirty = true;
351
+ return;
352
+ }
353
+ this.cancelPending();
354
+ const rawState = this.getState();
355
+ const state = this.compactState(rawState);
356
+ if (this.undoStack.at(-1) === state) {
357
+ this.emitChanged();
358
+ return;
359
+ }
288
360
  this.undoStack.push(state);
289
- if (this.undoStack.length > this.maxSize) {
361
+ while (this.undoStack.length > this.maxSize) {
290
362
  this.undoStack.shift();
291
363
  }
292
364
  this.redoStack = [];
365
+ this.trimToBudget();
366
+ this.events.emit("history:snapshot", {
367
+ bytes: rawState.length * 2,
368
+ totalBytes: this.snapshotBytes(),
369
+ entries: this.undoStack.length
370
+ });
293
371
  this.emitChanged();
294
372
  }
373
+ beginTransaction() {
374
+ if (this.transactionDepth === 0 && this.debounceTimer) this.saveImmediate();
375
+ this.transactionDepth += 1;
376
+ }
377
+ endTransaction() {
378
+ if (this.transactionDepth === 0) return;
379
+ this.transactionDepth -= 1;
380
+ if (this.transactionDepth === 0 && this.transactionDirty) {
381
+ this.transactionDirty = false;
382
+ this.saveImmediate();
383
+ }
384
+ }
385
+ async transaction(operation) {
386
+ this.beginTransaction();
387
+ try {
388
+ return await operation();
389
+ } finally {
390
+ this.endTransaction();
391
+ }
392
+ }
295
393
  async undo() {
296
394
  this.cancelPending();
297
- const state = this.undoStack.pop();
298
- if (!state) return;
299
- this.redoStack.push(this.getState());
395
+ const committed = this.undoStack.at(-1);
396
+ if (!committed) return;
397
+ const current = this.compactState(this.getState());
398
+ const currentIsCommitted = current === committed;
399
+ if (currentIsCommitted && this.undoStack.length < 2) return;
400
+ const target = currentIsCommitted ? this.undoStack[this.undoStack.length - 2] : committed;
300
401
  this.paused = true;
301
- await this.restoreState(state);
302
- this.paused = false;
303
- this.emitChanged();
402
+ try {
403
+ await this.restoreState(this.expandState(target));
404
+ if (currentIsCommitted) this.undoStack.pop();
405
+ this.redoStack.push(current);
406
+ this.trimToBudget();
407
+ } catch (error) {
408
+ this.events.emit("error", { message: "Failed to undo the last change", error });
409
+ throw error;
410
+ } finally {
411
+ this.paused = false;
412
+ this.emitChanged();
413
+ }
304
414
  }
305
415
  async redo() {
306
416
  this.cancelPending();
307
- const state = this.redoStack.pop();
417
+ const state = this.redoStack.at(-1);
308
418
  if (!state) return;
309
- this.undoStack.push(this.getState());
310
419
  this.paused = true;
311
- await this.restoreState(state);
312
- this.paused = false;
313
- this.emitChanged();
420
+ try {
421
+ await this.restoreState(this.expandState(state));
422
+ this.redoStack.pop();
423
+ if (this.undoStack.at(-1) !== state) this.undoStack.push(state);
424
+ this.trimToBudget();
425
+ } catch (error) {
426
+ this.events.emit("error", { message: "Failed to redo the last change", error });
427
+ throw error;
428
+ } finally {
429
+ this.paused = false;
430
+ this.emitChanged();
431
+ }
314
432
  }
315
433
  /** True while a restore (undo/redo/deserialize) is in flight. Managers that
316
434
  * react to canvas events should skip mutating history during this window. */
@@ -324,16 +442,22 @@ var HistoryManager = class {
324
442
  this.paused = false;
325
443
  }
326
444
  canUndo() {
327
- return this.undoStack.length > 0;
445
+ return this.undoStack.length > 1 || this.debounceTimer !== null;
328
446
  }
329
447
  canRedo() {
330
448
  return this.redoStack.length > 0;
331
449
  }
332
450
  clear() {
451
+ this.cancelPending();
333
452
  this.undoStack = [];
334
453
  this.redoStack = [];
454
+ this.assets.clear();
455
+ this.assetIds.clear();
335
456
  this.emitChanged();
336
457
  }
458
+ getSnapshotBytes() {
459
+ return this.snapshotBytes();
460
+ }
337
461
  cancelPending() {
338
462
  if (this.debounceTimer) {
339
463
  clearTimeout(this.debounceTimer);
@@ -341,9 +465,13 @@ var HistoryManager = class {
341
465
  }
342
466
  }
343
467
  dispose() {
344
- if (this.debounceTimer) {
345
- clearTimeout(this.debounceTimer);
346
- }
468
+ this.cancelPending();
469
+ this.transactionDepth = 0;
470
+ this.transactionDirty = false;
471
+ this.undoStack = [];
472
+ this.redoStack = [];
473
+ this.assets.clear();
474
+ this.assetIds.clear();
347
475
  }
348
476
  emitChanged() {
349
477
  this.events.emit("history:changed", {
@@ -351,6 +479,94 @@ var HistoryManager = class {
351
479
  canRedo: this.canRedo()
352
480
  });
353
481
  }
482
+ snapshotBytes() {
483
+ const stackBytes = [...this.undoStack, ...this.redoStack].reduce(
484
+ (total, state) => total + state.length * 2,
485
+ 0
486
+ );
487
+ const assetBytes = [...this.assets.values()].reduce(
488
+ (total, asset) => total + asset.length * 2,
489
+ 0
490
+ );
491
+ return stackBytes + assetBytes;
492
+ }
493
+ trimToBudget() {
494
+ this.pruneAssets();
495
+ while (this.undoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
496
+ this.undoStack.shift();
497
+ this.pruneAssets();
498
+ }
499
+ while (this.redoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
500
+ this.redoStack.shift();
501
+ this.pruneAssets();
502
+ }
503
+ }
504
+ /**
505
+ * History is internal and can content-address large raster strings without
506
+ * changing the public EditorState wire format. Unchanged images are retained
507
+ * once even when dozens of snapshots reference them.
508
+ */
509
+ compactState(state) {
510
+ let parsed;
511
+ try {
512
+ parsed = JSON.parse(state);
513
+ } catch {
514
+ return state;
515
+ }
516
+ const visit = (value) => {
517
+ if (typeof value === "string" && /^data:image\/(?:png|jpeg|webp);base64,/i.test(value)) {
518
+ let id = this.assetIds.get(value);
519
+ if (!id) {
520
+ id = `a${this.nextAssetId++}`;
521
+ this.assetIds.set(value, id);
522
+ this.assets.set(id, value);
523
+ }
524
+ return { [_HistoryManager.ASSET_KEY]: id };
525
+ }
526
+ if (Array.isArray(value)) return value.map(visit);
527
+ if (!value || typeof value !== "object") return value;
528
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)]));
529
+ };
530
+ return JSON.stringify(visit(parsed));
531
+ }
532
+ expandState(state) {
533
+ let parsed;
534
+ try {
535
+ parsed = JSON.parse(state);
536
+ } catch {
537
+ return state;
538
+ }
539
+ const visit = (value) => {
540
+ if (Array.isArray(value)) return value.map(visit);
541
+ if (!value || typeof value !== "object") return value;
542
+ const record = value;
543
+ const id = record[_HistoryManager.ASSET_KEY];
544
+ if (typeof id === "string" && Object.keys(record).length === 1) {
545
+ const asset = this.assets.get(id);
546
+ if (!asset) throw new Error(`Missing history raster asset: ${id}`);
547
+ return asset;
548
+ }
549
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, visit(entry)]));
550
+ };
551
+ return JSON.stringify(visit(parsed));
552
+ }
553
+ /**
554
+ * Runs on every commit, so it scans for the serialized reference marker
555
+ * instead of re-parsing every snapshot — parsing multi-megabyte raster states
556
+ * per save would cost more than the retention it reclaims. `JSON.stringify`
557
+ * emits the marker verbatim; the same text inside user data is escaped and
558
+ * therefore cannot match.
559
+ */
560
+ pruneAssets() {
561
+ if (this.assets.size === 0) return;
562
+ const states = [...this.undoStack, ...this.redoStack];
563
+ for (const [id, asset] of this.assets) {
564
+ const marker = `"${_HistoryManager.ASSET_KEY}":"${id}"`;
565
+ if (states.some((state) => state.includes(marker))) continue;
566
+ this.assets.delete(id);
567
+ this.assetIds.delete(asset);
568
+ }
569
+ }
354
570
  };
355
571
 
356
572
  // src/snapping.ts
@@ -570,6 +786,10 @@ var CropController = class {
570
786
  top: imgTop + (cropY - (image.cropY ?? 0)) * scaleY
571
787
  });
572
788
  image.setCoords();
789
+ if (s.prevAngle) {
790
+ image.rotate(s.prevAngle);
791
+ image.setCoords();
792
+ }
573
793
  this.finish();
574
794
  this.history.save();
575
795
  }
@@ -604,14 +824,18 @@ var STROKE2 = "#22c55e";
604
824
  var import_fabric2 = require("fabric");
605
825
  var MAX_TILES_PER_AXIS = 200;
606
826
  var PatternManager = class {
607
- constructor(canvas, layers, history) {
827
+ constructor(canvas, layers, history, events, sourceResolver) {
608
828
  this.canvas = canvas;
609
829
  this.layers = layers;
610
830
  this.history = history;
831
+ this.events = events;
832
+ this.sourceResolver = sourceResolver;
611
833
  }
612
834
  canvas;
613
835
  layers;
614
836
  history;
837
+ events;
838
+ sourceResolver;
615
839
  // Per-layer task chain. apply()/disable() both await an async setSrc on the
616
840
  // same fabric image; running two concurrently lets their setSrc resolutions
617
841
  // interleave (wrong image installed, original lost). Serialising per layer
@@ -659,6 +883,9 @@ var PatternManager = class {
659
883
  throw err;
660
884
  }
661
885
  this.history.save();
886
+ }).catch((error) => {
887
+ this.events.emit("error", { message: "Failed to apply image pattern", error });
888
+ throw error;
662
889
  });
663
890
  }
664
891
  /**
@@ -714,6 +941,9 @@ var PatternManager = class {
714
941
  delete layer.meta.pattern;
715
942
  this.canvas.requestRenderAll();
716
943
  this.history.save();
944
+ }).catch((error) => {
945
+ this.events.emit("error", { message: "Failed to clear image pattern", error });
946
+ throw error;
717
947
  });
718
948
  }
719
949
  /** Run `task` after any in-flight work for this layer, regardless of outcome. */
@@ -743,7 +973,8 @@ var PatternManager = class {
743
973
  cw,
744
974
  ch,
745
975
  tileW,
746
- tileH
976
+ tileH,
977
+ this.sourceResolver
747
978
  );
748
979
  await image.setSrc(dataUrl);
749
980
  image.set({
@@ -814,14 +1045,23 @@ function elementToDataURL(image) {
814
1045
  }
815
1046
  var IMAGE_CACHE_MAX = 16;
816
1047
  var imageCache = /* @__PURE__ */ new Map();
817
- function loadImage(src) {
1048
+ function loadPatternImage(src, resolver) {
818
1049
  const cached = imageCache.get(src);
819
1050
  if (cached) {
820
1051
  imageCache.delete(src);
821
1052
  imageCache.set(src, cached);
822
1053
  return cached;
823
1054
  }
824
- const promise = decodeImage(src);
1055
+ const promise = decodeImage(src).catch(async (originalError) => {
1056
+ if (!resolver) throw originalError;
1057
+ const resolved = await resolver(src);
1058
+ if (!resolved || resolved === src) {
1059
+ throw new Error("Pattern source resolver did not return a usable alternate URL", {
1060
+ cause: originalError
1061
+ });
1062
+ }
1063
+ return decodeImage(resolved);
1064
+ });
825
1065
  promise.catch(() => {
826
1066
  if (imageCache.get(src) === promise) imageCache.delete(src);
827
1067
  });
@@ -844,8 +1084,8 @@ function decodeImage(src) {
844
1084
  function clearPatternImageCache() {
845
1085
  imageCache.clear();
846
1086
  }
847
- async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH) {
848
- const img = await loadImage(src);
1087
+ async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
1088
+ const img = await loadPatternImage(src, sourceResolver);
849
1089
  const off = document.createElement("canvas");
850
1090
  off.width = Math.max(1, Math.round(targetW));
851
1091
  off.height = Math.max(1, Math.round(targetH));
@@ -905,16 +1145,18 @@ function mod2(n) {
905
1145
  // src/utils/units.ts
906
1146
  var MM_PER_INCH = 25.4;
907
1147
  var UnitConverter = class {
1148
+ unit;
1149
+ dpi;
908
1150
  constructor(unit = "px", dpi = 72) {
909
1151
  this.unit = unit;
910
- this.dpi = dpi;
1152
+ this.dpi = 72;
1153
+ this.setDpi(dpi);
911
1154
  }
912
- unit;
913
- dpi;
914
1155
  setUnit(unit) {
915
1156
  this.unit = unit;
916
1157
  }
917
1158
  setDpi(dpi) {
1159
+ if (!Number.isFinite(dpi) || dpi <= 0) throw new Error("DPI must be a positive number");
918
1160
  this.dpi = dpi;
919
1161
  }
920
1162
  getUnit() {
@@ -949,38 +1191,92 @@ var UnitConverter = class {
949
1191
 
950
1192
  // src/serialization.ts
951
1193
  var import_fabric3 = require("fabric");
952
- var VERSION = "1.0.0";
1194
+
1195
+ // src/utils/color.ts
1196
+ var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
1197
+ function isCssColor(value, allowEmpty = false) {
1198
+ if (typeof value !== "string") return false;
1199
+ const trimmed = value.trim();
1200
+ if (!trimmed) return allowEmpty;
1201
+ return CSS_COLOR.test(trimmed);
1202
+ }
1203
+
1204
+ // src/serialization.ts
1205
+ var VERSION = "2.0.0";
953
1206
  function serializeEditor(editor) {
954
1207
  return {
955
1208
  version: VERSION,
956
1209
  canvas: {
957
1210
  width: editor.canvas.getWidth(),
958
- height: editor.canvas.getHeight()
1211
+ height: editor.canvas.getHeight(),
1212
+ unit: editor.units.getUnit(),
1213
+ dpi: editor.units.getDpi()
959
1214
  },
960
1215
  layers: editor.layers.getAll().map((layer) => layer.serialize()),
961
1216
  // The configured design background, not the live canvas value (which is
962
1217
  // forced transparent while a mockup preview is active).
963
1218
  background: editor.getDesignBackground(),
1219
+ backgroundImage: editor.getDesignBackgroundImage() ? editor.getDesignBackgroundImage().toObject() : null,
1220
+ backgroundImageOptions: editor.getBackgroundImageOptions(),
964
1221
  mockup: editor.getMockup()
965
1222
  };
966
1223
  }
967
1224
  async function deserializeEditor(editor, state) {
1225
+ if (!state || !state.canvas || !Array.isArray(state.layers)) {
1226
+ throw new Error("Invalid editor state");
1227
+ }
1228
+ 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)) {
1229
+ throw new Error("Invalid editor canvas settings");
1230
+ }
1231
+ const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
1232
+ if (!Number.isFinite(major) || major > 2) {
1233
+ throw new Error(`Unsupported editor state version: ${state.version}`);
1234
+ }
1235
+ if (state.background !== void 0 && !isCssColor(state.background, true)) {
1236
+ throw new Error("Invalid editor background color");
1237
+ }
1238
+ const staged = await Promise.all(
1239
+ state.layers.map(async (serialized) => {
1240
+ const fabricObject = (await import_fabric3.util.enlivenObjects([serialized.fabricObject]))[0];
1241
+ if (!fabricObject) {
1242
+ const source = serialized.fabricObject.src;
1243
+ if (typeof source === "string" && source.startsWith("blob:")) {
1244
+ throw new Error(`Failed to restore expired object URL: ${source}`);
1245
+ }
1246
+ throw new Error(`Failed to restore layer: ${serialized.id}`);
1247
+ }
1248
+ return { serialized, fabricObject };
1249
+ })
1250
+ );
1251
+ const stagedBackground = state.backgroundImage ? (await import_fabric3.util.enlivenObjects([state.backgroundImage]))[0] : null;
1252
+ if (state.backgroundImage && !stagedBackground) {
1253
+ const source = state.backgroundImage.src;
1254
+ if (typeof source === "string" && source.startsWith("blob:")) {
1255
+ throw new Error(`Failed to restore expired background object URL: ${source}`);
1256
+ }
1257
+ throw new Error("Failed to restore background image");
1258
+ }
1259
+ editor.crop.cancel();
1260
+ editor.masks.detach();
968
1261
  editor.layers.clear();
1262
+ if (state.canvas.unit) editor.units.setUnit(state.canvas.unit);
1263
+ if (state.canvas.dpi !== void 0) editor.units.setDpi(state.canvas.dpi);
969
1264
  editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
970
- if (state.background) {
1265
+ if (state.background !== void 0) {
971
1266
  editor.setBackground(state.background);
972
1267
  }
973
- if (state.mockup !== void 0) {
974
- editor.setMockup(state.mockup);
975
- }
976
- for (const serializedLayer of state.layers) {
977
- await restoreLayer(editor, serializedLayer);
1268
+ editor.setBackgroundImageObject(
1269
+ stagedBackground ?? null,
1270
+ false,
1271
+ state.backgroundImageOptions ?? null
1272
+ );
1273
+ editor.setMockup(state.mockup ?? null);
1274
+ for (const item of staged) {
1275
+ restoreLayer(editor, item.serialized, item.fabricObject);
978
1276
  }
979
1277
  editor.canvas.requestRenderAll();
980
1278
  }
981
- async function restoreLayer(editor, serialized) {
982
- const objects = await import_fabric3.util.enlivenObjects([serialized.fabricObject]);
983
- const fabricObject = objects[0];
1279
+ function restoreLayer(editor, serialized, fabricObject) {
984
1280
  const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
985
1281
  if (serialized.meta) {
986
1282
  layer.meta = serialized.meta;
@@ -998,15 +1294,216 @@ async function restoreLayer(editor, serialized) {
998
1294
  }
999
1295
 
1000
1296
  // src/export.ts
1297
+ var import_fabric4 = require("fabric");
1298
+
1299
+ // src/displacement.ts
1300
+ var CHANNEL_INDEX = {
1301
+ red: 0,
1302
+ green: 1,
1303
+ blue: 2,
1304
+ alpha: 3
1305
+ };
1306
+ function finiteScale(value, fallback, label) {
1307
+ const resolved = value ?? fallback;
1308
+ if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);
1309
+ return resolved;
1310
+ }
1311
+ function sample(source, width, height, x, y, channel) {
1312
+ const clampedX = Math.max(0, Math.min(width - 1, x));
1313
+ const clampedY = Math.max(0, Math.min(height - 1, y));
1314
+ const x0 = Math.floor(clampedX);
1315
+ const y0 = Math.floor(clampedY);
1316
+ const x1 = Math.min(width - 1, x0 + 1);
1317
+ const y1 = Math.min(height - 1, y0 + 1);
1318
+ const tx = clampedX - x0;
1319
+ const ty = clampedY - y0;
1320
+ const top = source[(y0 * width + x0) * 4 + channel] * (1 - tx) + source[(y0 * width + x1) * 4 + channel] * tx;
1321
+ const bottom = source[(y1 * width + x0) * 4 + channel] * (1 - tx) + source[(y1 * width + x1) * 4 + channel] * tx;
1322
+ return top * (1 - ty) + bottom * ty;
1323
+ }
1324
+ function displaceRgba(source, map, width, height, options) {
1325
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
1326
+ throw new Error("Displacement dimensions must be positive integers");
1327
+ }
1328
+ const expectedLength = width * height * 4;
1329
+ if (source.length !== expectedLength || map.length !== expectedLength) {
1330
+ throw new Error("Displacement source and map must match the requested dimensions");
1331
+ }
1332
+ const scaleX = finiteScale(options.scaleX, 10, "Displacement scaleX");
1333
+ const scaleY = finiteScale(options.scaleY, 10, "Displacement scaleY");
1334
+ const channelX = CHANNEL_INDEX[options.channelX ?? "red"];
1335
+ const channelY = CHANNEL_INDEX[options.channelY ?? "green"];
1336
+ const output = new Uint8ClampedArray(expectedLength);
1337
+ for (let y = 0; y < height; y += 1) {
1338
+ for (let x = 0; x < width; x += 1) {
1339
+ const offset = (y * width + x) * 4;
1340
+ const sourceX = x + (map[offset + channelX] - 128) / 127 * scaleX;
1341
+ const sourceY = y + (map[offset + channelY] - 128) / 127 * scaleY;
1342
+ for (let channel = 0; channel < 4; channel += 1) {
1343
+ output[offset + channel] = Math.round(
1344
+ sample(source, width, height, sourceX, sourceY, channel)
1345
+ );
1346
+ }
1347
+ }
1348
+ }
1349
+ return output;
1350
+ }
1351
+
1352
+ // src/export.ts
1353
+ function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
1354
+ const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
1355
+ const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));
1356
+ const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));
1357
+ const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));
1358
+ return { left, top, width: right - left, height: bottom - top };
1359
+ }
1360
+ function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHeight) {
1361
+ if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
1362
+ throw new Error("Cover dimensions must be positive");
1363
+ }
1364
+ const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
1365
+ const width = sourceWidth * scale;
1366
+ const height = sourceHeight * scale;
1367
+ return {
1368
+ left: (targetWidth - width) / 2,
1369
+ top: (targetHeight - height) / 2,
1370
+ width,
1371
+ height
1372
+ };
1373
+ }
1374
+ function canvasElementToBlob(output, format, quality) {
1375
+ const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
1376
+ return new Promise((resolve, reject) => {
1377
+ output.toBlob(
1378
+ (blob) => blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`)),
1379
+ mime,
1380
+ quality
1381
+ );
1382
+ });
1383
+ }
1001
1384
  async function exportPNG(canvas, options = {}) {
1002
1385
  const { multiplier = 1, format = "png", quality = 1 } = options;
1003
- const dataUrl = canvas.toDataURL({
1004
- format,
1005
- multiplier,
1006
- quality
1386
+ const output = canvas.toCanvasElement(multiplier);
1387
+ return canvasElementToBlob(output, format, quality);
1388
+ }
1389
+ async function exportIsolatedPNG(source, objects, options = {}) {
1390
+ const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
1391
+ const canvas = new import_fabric4.StaticCanvas(element, {
1392
+ width: options.width ?? source.getWidth(),
1393
+ height: options.height ?? source.getHeight(),
1394
+ backgroundColor: options.backgroundColor || void 0
1007
1395
  });
1008
- const response = await fetch(dataUrl);
1009
- return response.blob();
1396
+ try {
1397
+ const clones = options.cloneObjects === false ? objects : await Promise.all(objects.map((object) => object.clone()));
1398
+ if (clones.length) canvas.add(...clones);
1399
+ if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();
1400
+ canvas.requestRenderAll();
1401
+ return await exportPNG(canvas, options);
1402
+ } finally {
1403
+ canvas.dispose();
1404
+ }
1405
+ }
1406
+ async function exportMockup(canvas, mockup, options = {}) {
1407
+ const { multiplier = 1, format = "png", quality = 1 } = options;
1408
+ const design = canvas.toCanvasElement(multiplier);
1409
+ const output = design.ownerDocument.createElement("canvas");
1410
+ output.width = design.width;
1411
+ output.height = design.height;
1412
+ const context = output.getContext("2d");
1413
+ if (!context) throw new Error("2D canvas context is unavailable");
1414
+ const loadImage = (url) => new Promise((resolve, reject) => {
1415
+ const element = new Image();
1416
+ element.crossOrigin = "anonymous";
1417
+ element.onload = () => resolve(element);
1418
+ element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
1419
+ element.src = url;
1420
+ });
1421
+ const drawCover = (image, targetContext = context) => {
1422
+ const placement = computeCoverPlacement(
1423
+ image.naturalWidth || image.width,
1424
+ image.naturalHeight || image.height,
1425
+ output.width,
1426
+ output.height
1427
+ );
1428
+ targetContext.drawImage(
1429
+ image,
1430
+ placement.left,
1431
+ placement.top,
1432
+ placement.width,
1433
+ placement.height
1434
+ );
1435
+ };
1436
+ const scratch = [design];
1437
+ try {
1438
+ drawCover(await loadImage(mockup.image));
1439
+ let compositedDesign = design;
1440
+ if (mockup.displacement) {
1441
+ const sourceContext = design.getContext("2d");
1442
+ if (!sourceContext) throw new Error("2D design context is unavailable");
1443
+ const mapCanvas = design.ownerDocument.createElement("canvas");
1444
+ scratch.push(mapCanvas);
1445
+ mapCanvas.width = design.width;
1446
+ mapCanvas.height = design.height;
1447
+ const mapContext = mapCanvas.getContext("2d");
1448
+ if (!mapContext) throw new Error("2D displacement-map context is unavailable");
1449
+ drawCover(await loadImage(mockup.displacement.image), mapContext);
1450
+ const warped = design.ownerDocument.createElement("canvas");
1451
+ scratch.push(warped);
1452
+ warped.width = design.width;
1453
+ warped.height = design.height;
1454
+ const warpedContext = warped.getContext("2d");
1455
+ if (!warpedContext) throw new Error("2D displaced-design context is unavailable");
1456
+ let sourcePixels;
1457
+ let mapPixels;
1458
+ try {
1459
+ sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;
1460
+ mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;
1461
+ } catch (error) {
1462
+ throw new Error("Failed to apply mockup displacement map; verify image CORS access", {
1463
+ cause: error
1464
+ });
1465
+ }
1466
+ const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {
1467
+ ...mockup.displacement,
1468
+ scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,
1469
+ scaleY: (mockup.displacement.scaleY ?? 10) * multiplier
1470
+ });
1471
+ const imageData = warpedContext.createImageData(design.width, design.height);
1472
+ imageData.data.set(pixels);
1473
+ warpedContext.putImageData(imageData, 0, 0);
1474
+ compositedDesign = warped;
1475
+ }
1476
+ context.save();
1477
+ if (mockup.printArea && mockup.clipToPrintArea !== false) {
1478
+ const clip = computePrintAreaClip(
1479
+ mockup.printArea,
1480
+ output.width / canvas.getWidth(),
1481
+ output.height / canvas.getHeight(),
1482
+ output.width,
1483
+ output.height
1484
+ );
1485
+ context.beginPath();
1486
+ context.rect(clip.left, clip.top, clip.width, clip.height);
1487
+ context.clip();
1488
+ }
1489
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
1490
+ context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
1491
+ context.drawImage(compositedDesign, 0, 0);
1492
+ context.restore();
1493
+ if (mockup.overlay) {
1494
+ context.save();
1495
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
1496
+ context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
1497
+ drawCover(await loadImage(mockup.overlay.image));
1498
+ context.restore();
1499
+ }
1500
+ return await canvasElementToBlob(output, format, quality);
1501
+ } finally {
1502
+ for (const element of scratch) {
1503
+ element.width = 0;
1504
+ element.height = 0;
1505
+ }
1506
+ }
1010
1507
  }
1011
1508
  function exportSVG(canvas) {
1012
1509
  return canvas.toSVG();
@@ -1015,9 +1512,583 @@ function exportDataURL(canvas, format = "png", multiplier = 1) {
1015
1512
  return canvas.toDataURL({ format, multiplier });
1016
1513
  }
1017
1514
 
1515
+ // src/utils/svg.ts
1516
+ function escapeXml(value) {
1517
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1518
+ }
1519
+ function sanitizeSvg(svg) {
1520
+ const document2 = new DOMParser().parseFromString(svg, "image/svg+xml");
1521
+ if (document2.querySelector("parsererror")) throw new Error("Invalid template SVG");
1522
+ document2.querySelectorAll("script, foreignObject, iframe, object, embed, link, style").forEach((node) => node.remove());
1523
+ document2.querySelectorAll("*").forEach((node) => {
1524
+ for (const attribute of [...node.attributes]) {
1525
+ const name = attribute.name.toLowerCase();
1526
+ const value = attribute.value.trim().toLowerCase();
1527
+ const isLink = name === "href" || name === "xlink:href" || name === "src";
1528
+ const safeLink = value.startsWith("#") || /^data:image\/(?:png|jpeg|webp|gif);base64,/.test(value);
1529
+ if (name.startsWith("on") || isLink && !safeLink || /url\s*\(/.test(value) && !/url\s*\(\s*['"]?#/.test(value) || /(?:javascript:|expression\s*\()/.test(value)) {
1530
+ node.removeAttribute(attribute.name);
1531
+ }
1532
+ }
1533
+ });
1534
+ return new XMLSerializer().serializeToString(document2.documentElement);
1535
+ }
1536
+
1537
+ // src/fonts.ts
1538
+ function fontSource(source) {
1539
+ return /^(?:url|local)\(/.test(source.trim()) ? source : `url(${JSON.stringify(source)})`;
1540
+ }
1541
+ function mimeForSource(source) {
1542
+ const path = source.split(/[?#]/)[0].toLowerCase();
1543
+ if (path.endsWith(".woff2")) return "font/woff2";
1544
+ if (path.endsWith(".woff")) return "font/woff";
1545
+ if (path.endsWith(".otf")) return "font/otf";
1546
+ return "font/ttf";
1547
+ }
1548
+ function arrayBufferToBase64(buffer) {
1549
+ const bytes = new Uint8Array(buffer);
1550
+ let binary = "";
1551
+ for (let index = 0; index < bytes.length; index += 32768) {
1552
+ binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
1553
+ }
1554
+ return btoa(binary);
1555
+ }
1556
+ function cssString(value) {
1557
+ return JSON.stringify(value).replace(/[<>&]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
1558
+ }
1559
+ var SAFE_WEIGHT = /^(?:normal|bold|bolder|lighter|[1-9]\d{0,2}(?:\s+[1-9]\d{0,2})?)$/i;
1560
+ var SAFE_STYLE = /^(?:normal|italic|oblique(?:\s+-?\d+(?:\.\d+)?deg)?)$/i;
1561
+ var SAFE_DISPLAY = /^(?:auto|block|swap|fallback|optional)$/i;
1562
+ function cssKeyword(value, pattern, fallback) {
1563
+ const trimmed = value?.trim();
1564
+ return trimmed && pattern.test(trimmed) ? trimmed : fallback;
1565
+ }
1566
+ function sourceUrl(source) {
1567
+ const trimmed = source.trim();
1568
+ if (trimmed.startsWith("data:")) return trimmed;
1569
+ const match = trimmed.match(/^url\(\s*(['"]?)(.*?)\1\s*\)/i);
1570
+ if (match) return match[2];
1571
+ if (/^local\(/i.test(trimmed)) return null;
1572
+ return trimmed;
1573
+ }
1574
+ function localName(source) {
1575
+ const match = source.trim().match(/^local\(\s*(['"]?)([^)"'{};]*)\1\s*\)$/i);
1576
+ return match ? match[2].trim() || null : null;
1577
+ }
1578
+ var FontRegistry = class {
1579
+ definitions = /* @__PURE__ */ new Map();
1580
+ loads = /* @__PURE__ */ new Map();
1581
+ register(definition) {
1582
+ if (!definition.family.trim() || !definition.source.trim()) {
1583
+ throw new Error("Font family and source are required");
1584
+ }
1585
+ this.definitions.set(definition.family, { ...definition });
1586
+ this.loads.delete(definition.family);
1587
+ }
1588
+ unregister(family) {
1589
+ this.loads.delete(family);
1590
+ return this.definitions.delete(family);
1591
+ }
1592
+ getAll() {
1593
+ return [...this.definitions.values()].map((definition) => ({ ...definition }));
1594
+ }
1595
+ load(family) {
1596
+ const cached = this.loads.get(family);
1597
+ if (cached) return cached;
1598
+ const definition = this.definitions.get(family);
1599
+ if (!definition) return Promise.reject(new Error(`Font is not registered: ${family}`));
1600
+ if (typeof FontFace === "undefined" || typeof document === "undefined") {
1601
+ return Promise.reject(new Error("Font loading requires a browser FontFace API"));
1602
+ }
1603
+ const promise = new FontFace(definition.family, fontSource(definition.source), {
1604
+ weight: definition.weight,
1605
+ style: definition.style,
1606
+ display: definition.display
1607
+ }).load().then((font) => {
1608
+ document.fonts.add(font);
1609
+ return font;
1610
+ });
1611
+ promise.catch(() => {
1612
+ if (this.loads.get(family) === promise) this.loads.delete(family);
1613
+ });
1614
+ this.loads.set(family, promise);
1615
+ return promise;
1616
+ }
1617
+ async ready() {
1618
+ if (this.definitions.size === 0) return;
1619
+ await Promise.all([...this.definitions.keys()].map((family) => this.load(family)));
1620
+ await document.fonts.ready;
1621
+ }
1622
+ async getEmbeddedCss() {
1623
+ const rules = await Promise.all(
1624
+ this.getAll().map(async (definition) => {
1625
+ const url = sourceUrl(definition.source);
1626
+ let cssSource;
1627
+ if (url && !url.startsWith("data:")) {
1628
+ const response = await fetch(url);
1629
+ if (!response.ok) throw new Error(`Failed to fetch font: ${url}`);
1630
+ const data = arrayBufferToBase64(await response.arrayBuffer());
1631
+ const mime = response.headers.get("content-type") || mimeForSource(url);
1632
+ cssSource = `url(${cssString(`data:${mime};base64,${data}`)})`;
1633
+ } else if (url) {
1634
+ cssSource = `url(${cssString(url)})`;
1635
+ } else {
1636
+ const name = localName(definition.source);
1637
+ if (!name) {
1638
+ throw new Error(`Unsupported font source for embedding: ${definition.family}`);
1639
+ }
1640
+ cssSource = `local(${cssString(name)})`;
1641
+ }
1642
+ const weight = cssKeyword(definition.weight, SAFE_WEIGHT, "normal");
1643
+ const style = cssKeyword(definition.style, SAFE_STYLE, "normal");
1644
+ const display = cssKeyword(definition.display, SAFE_DISPLAY, "swap");
1645
+ return `@font-face{font-family:${cssString(definition.family)};src:${cssSource};font-weight:${weight};font-style:${style};font-display:${display}}`;
1646
+ })
1647
+ );
1648
+ return rules.join("\n");
1649
+ }
1650
+ };
1651
+
1652
+ // src/licensing.ts
1653
+ function domainMatches(hostname, pattern) {
1654
+ const host = hostname.toLowerCase();
1655
+ const expected = pattern.toLowerCase();
1656
+ if (expected.startsWith("*.")) {
1657
+ const suffix = expected.slice(1);
1658
+ return host.endsWith(suffix) && host.length > suffix.length;
1659
+ }
1660
+ return host === expected;
1661
+ }
1662
+ function isLocal(hostname) {
1663
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
1664
+ }
1665
+ var LicenseManager = class {
1666
+ constructor(config = {}) {
1667
+ this.config = config;
1668
+ const hostname = config.hostname ?? globalThis.location?.hostname ?? "localhost";
1669
+ const environment = config.environment ?? "development";
1670
+ if (environment !== "production" || isLocal(hostname)) {
1671
+ this.status = { state: "exempt", payload: null };
1672
+ this.readyPromise = Promise.resolve(this.status);
1673
+ } else if (!config.key) {
1674
+ this.status = { state: "community", payload: null };
1675
+ this.readyPromise = Promise.resolve(this.status);
1676
+ } else {
1677
+ this.status = { state: "checking", payload: null };
1678
+ this.readyPromise = this.validate(config.key, hostname);
1679
+ }
1680
+ }
1681
+ config;
1682
+ status;
1683
+ readyPromise;
1684
+ getStatus() {
1685
+ return this.status;
1686
+ }
1687
+ ready() {
1688
+ return this.readyPromise;
1689
+ }
1690
+ hasFeature(feature) {
1691
+ return this.status.state === "valid" && (this.status.payload.features ?? []).includes(feature);
1692
+ }
1693
+ track(name) {
1694
+ this.config.onUsage?.({
1695
+ name,
1696
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1697
+ licenseId: this.status.state === "valid" ? this.status.payload.id : void 0
1698
+ });
1699
+ }
1700
+ async validate(key, hostname) {
1701
+ const payload = await this.config.verifyOffline?.(key) ?? null;
1702
+ if (!payload) return this.status = { state: "invalid", payload: null };
1703
+ if (payload.expiresAt && Date.parse(payload.expiresAt) < Date.now()) {
1704
+ return this.status = { state: "expired", payload };
1705
+ }
1706
+ if (!payload.domains.some((domain) => domainMatches(hostname, domain))) {
1707
+ return this.status = { state: "domain-mismatch", payload };
1708
+ }
1709
+ return this.status = { state: "valid", payload };
1710
+ }
1711
+ };
1712
+
1713
+ // src/project.ts
1714
+ var ProjectManager = class {
1715
+ constructor(editor) {
1716
+ this.editor = editor;
1717
+ const first = { id: generateId(), name: "Page 1", state: editor.toJSON() };
1718
+ this.pages = [first];
1719
+ this.activePageId = first.id;
1720
+ }
1721
+ editor;
1722
+ pages;
1723
+ activePageId;
1724
+ getAll() {
1725
+ return this.pages.map(({ id, name }) => ({ id, name }));
1726
+ }
1727
+ getActivePageId() {
1728
+ return this.activePageId;
1729
+ }
1730
+ add(name = `Page ${this.pages.length + 1}`, cloneCurrent = false) {
1731
+ this.saveCurrent();
1732
+ const source = structuredClone(this.editor.toJSON());
1733
+ const state = cloneCurrent ? source : this.blankState(source);
1734
+ const page = { id: generateId(), name, state };
1735
+ this.pages.push(page);
1736
+ this.emitChanged();
1737
+ return page.id;
1738
+ }
1739
+ async switchTo(id) {
1740
+ if (id === this.activePageId) return true;
1741
+ const page = this.pages.find((candidate) => candidate.id === id);
1742
+ if (!page) return false;
1743
+ this.saveCurrent();
1744
+ await this.editor.fromJSON(structuredClone(page.state));
1745
+ this.activePageId = id;
1746
+ this.emitChanged();
1747
+ return true;
1748
+ }
1749
+ async duplicate(id) {
1750
+ this.saveCurrent();
1751
+ const source = this.pages.find((page2) => page2.id === id);
1752
+ if (!source) return null;
1753
+ const page = {
1754
+ id: generateId(),
1755
+ name: `${source.name} copy`,
1756
+ state: structuredClone(source.state)
1757
+ };
1758
+ const index = this.pages.indexOf(source);
1759
+ this.pages.splice(index + 1, 0, page);
1760
+ this.emitChanged();
1761
+ return page.id;
1762
+ }
1763
+ async remove(id) {
1764
+ if (this.pages.length === 1) return false;
1765
+ const index = this.pages.findIndex((page) => page.id === id);
1766
+ if (index < 0) return false;
1767
+ if (id === this.activePageId) {
1768
+ const next = this.pages[index + 1] ?? this.pages[index - 1];
1769
+ await this.editor.fromJSON(structuredClone(next.state));
1770
+ this.activePageId = next.id;
1771
+ }
1772
+ this.pages.splice(index, 1);
1773
+ this.emitChanged();
1774
+ return true;
1775
+ }
1776
+ rename(id, name) {
1777
+ const page = this.pages.find((candidate) => candidate.id === id);
1778
+ const trimmed = name.trim();
1779
+ if (!page || !trimmed || page.name === trimmed) return false;
1780
+ page.name = trimmed;
1781
+ this.emitChanged();
1782
+ return true;
1783
+ }
1784
+ reorder(id, newIndex) {
1785
+ const index = this.pages.findIndex((page2) => page2.id === id);
1786
+ if (index < 0 || !Number.isFinite(newIndex)) return false;
1787
+ const target = Math.max(0, Math.min(this.pages.length - 1, Math.round(newIndex)));
1788
+ if (target === index) return false;
1789
+ const [page] = this.pages.splice(index, 1);
1790
+ this.pages.splice(target, 0, page);
1791
+ this.emitChanged();
1792
+ return true;
1793
+ }
1794
+ toJSON() {
1795
+ this.saveCurrent();
1796
+ return {
1797
+ version: "1.0.0",
1798
+ activePageId: this.activePageId,
1799
+ pages: structuredClone(this.pages)
1800
+ };
1801
+ }
1802
+ async fromJSON(project) {
1803
+ if (project?.version !== "1.0.0" || !Array.isArray(project.pages) || project.pages.length === 0 || !project.pages.some((page) => page.id === project.activePageId)) {
1804
+ throw new Error("Invalid project state");
1805
+ }
1806
+ const pages = structuredClone(project.pages);
1807
+ const active = pages.find((page) => page.id === project.activePageId);
1808
+ await this.editor.fromJSON(structuredClone(active.state));
1809
+ this.pages = pages;
1810
+ this.activePageId = active.id;
1811
+ this.emitChanged();
1812
+ }
1813
+ saveCurrent() {
1814
+ const page = this.pages.find((candidate) => candidate.id === this.activePageId);
1815
+ if (page) page.state = structuredClone(this.editor.toJSON());
1816
+ }
1817
+ blankState(source) {
1818
+ return { ...source, layers: [], mockup: null };
1819
+ }
1820
+ emitChanged() {
1821
+ this.editor.events.emit("project:changed", {
1822
+ activePageId: this.activePageId,
1823
+ pages: this.getAll()
1824
+ });
1825
+ }
1826
+ };
1827
+
1828
+ // src/mask.ts
1829
+ var import_fabric5 = require("fabric");
1830
+ var MaskRefinementError = class extends Error {
1831
+ constructor(code, message, cause) {
1832
+ super(message);
1833
+ this.code = code;
1834
+ this.cause = cause;
1835
+ this.name = "MaskRefinementError";
1836
+ }
1837
+ code;
1838
+ cause;
1839
+ };
1840
+ var MaskController = class {
1841
+ constructor(editor) {
1842
+ this.editor = editor;
1843
+ }
1844
+ editor;
1845
+ backing = null;
1846
+ context = null;
1847
+ layerId = null;
1848
+ brush = null;
1849
+ previousPoint = null;
1850
+ strokeBackup = null;
1851
+ strokeStartedAt = null;
1852
+ lastInteractionLatencyMs = 0;
1853
+ refinement = null;
1854
+ disposed = false;
1855
+ async create(width = this.editor.canvas.getWidth(), height = this.editor.canvas.getHeight()) {
1856
+ this.assertActive();
1857
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
1858
+ throw new Error("Mask dimensions must be positive integers");
1859
+ }
1860
+ const backing = this.makeCanvas(width, height);
1861
+ const image = new import_fabric5.FabricImage(backing, {
1862
+ left: 0,
1863
+ top: 0,
1864
+ originX: "left",
1865
+ originY: "top",
1866
+ selectable: false
1867
+ });
1868
+ const layer = this.editor.layers.add("mask", image, "Mask");
1869
+ layer.meta.mask = { width, height, revision: 0 };
1870
+ this.editor.history.saveImmediate();
1871
+ this.attachBacking(layer.id, backing);
1872
+ return layer;
1873
+ }
1874
+ attach(layerId) {
1875
+ this.assertActive();
1876
+ this.cancelStroke();
1877
+ const layer = this.requireMask(layerId);
1878
+ const image = layer.fabricObject;
1879
+ const width = layer.meta.mask?.width ?? image.width ?? this.editor.canvas.getWidth();
1880
+ const height = layer.meta.mask?.height ?? image.height ?? this.editor.canvas.getHeight();
1881
+ const backing = this.makeCanvas(width, height);
1882
+ const context = backing.getContext("2d");
1883
+ if (!context) throw new Error("2D mask context is unavailable");
1884
+ const element = image.getElement();
1885
+ if (element) context.drawImage(element, 0, 0, width, height);
1886
+ this.attachBacking(layerId, backing);
1887
+ }
1888
+ beginStroke(options) {
1889
+ this.assertActive();
1890
+ if (!this.context || !this.backing || !this.layerId) throw new Error("Attach a mask first");
1891
+ if (this.brush) throw new Error("A mask stroke is already active");
1892
+ if (!Number.isFinite(options.size) || options.size <= 0) {
1893
+ throw new Error("Mask brush size must be positive");
1894
+ }
1895
+ this.brush = { ...options, hardness: clamp(options.hardness, 0, 1) };
1896
+ this.previousPoint = null;
1897
+ this.strokeBackup = this.context.getImageData(0, 0, this.backing.width, this.backing.height);
1898
+ this.strokeStartedAt = performance.now();
1899
+ }
1900
+ addPoint(point) {
1901
+ if (!this.brush || !this.context || !this.backing || !this.layerId) {
1902
+ throw new Error("No active mask stroke");
1903
+ }
1904
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
1905
+ const previous = this.previousPoint ?? point;
1906
+ const distance = Math.hypot(point.x - previous.x, point.y - previous.y);
1907
+ const step = Math.max(1, this.brush.size / 4);
1908
+ const samples = Math.max(1, Math.ceil(distance / step));
1909
+ for (let index = 0; index <= samples; index += 1) {
1910
+ const ratio = index / samples;
1911
+ this.drawDot({
1912
+ x: previous.x + (point.x - previous.x) * ratio,
1913
+ y: previous.y + (point.y - previous.y) * ratio
1914
+ });
1915
+ }
1916
+ this.previousPoint = point;
1917
+ this.requireMask(this.layerId).fabricObject.setElement(this.backing);
1918
+ this.editor.canvas.requestRenderAll();
1919
+ }
1920
+ async endStroke() {
1921
+ if (!this.brush || !this.backing || !this.layerId) return;
1922
+ const layerId = this.layerId;
1923
+ this.brush = null;
1924
+ this.previousPoint = null;
1925
+ this.strokeBackup = null;
1926
+ const dataUrl = this.backing.toDataURL("image/png");
1927
+ await this.editor.history.transaction(async () => {
1928
+ await this.editor.replaceImageSource(layerId, dataUrl);
1929
+ const layer = this.requireMask(layerId);
1930
+ if (layer.meta.mask) layer.meta.mask.revision += 1;
1931
+ });
1932
+ this.attach(layerId);
1933
+ if (this.strokeStartedAt !== null) {
1934
+ this.lastInteractionLatencyMs = performance.now() - this.strokeStartedAt;
1935
+ this.strokeStartedAt = null;
1936
+ }
1937
+ }
1938
+ cancelStroke() {
1939
+ if (this.strokeBackup && this.context && this.backing && this.layerId) {
1940
+ this.context.putImageData(this.strokeBackup, 0, 0);
1941
+ this.requireMask(this.layerId).fabricObject.setElement(this.backing);
1942
+ this.editor.canvas.requestRenderAll();
1943
+ }
1944
+ this.brush = null;
1945
+ this.previousPoint = null;
1946
+ this.strokeBackup = null;
1947
+ this.strokeStartedAt = null;
1948
+ }
1949
+ isStrokeActive() {
1950
+ return this.brush !== null;
1951
+ }
1952
+ activeLayerId() {
1953
+ return this.layerId;
1954
+ }
1955
+ detach(layerId) {
1956
+ if (layerId && this.layerId !== layerId) return;
1957
+ this.cancelStroke();
1958
+ this.cancelRefinement();
1959
+ this.backing = null;
1960
+ this.context = null;
1961
+ this.layerId = null;
1962
+ }
1963
+ async refine(layerId, provider, prompts, options = {}) {
1964
+ this.assertActive();
1965
+ const layer = this.editor.layers.get(layerId);
1966
+ if (!layer || layer.type !== "mask") {
1967
+ throw new MaskRefinementError("not-found", `Mask layer not found: ${layerId}`);
1968
+ }
1969
+ this.cancelRefinement();
1970
+ const controller = new AbortController();
1971
+ this.refinement = controller;
1972
+ const abort = () => controller.abort(options.signal?.reason);
1973
+ options.signal?.addEventListener("abort", abort, { once: true });
1974
+ if (options.signal?.aborted) abort();
1975
+ try {
1976
+ const image = layer.fabricObject;
1977
+ const result = await provider.refine(
1978
+ {
1979
+ mask: image.getSrc(),
1980
+ width: layer.meta.mask?.width ?? image.width ?? 1,
1981
+ height: layer.meta.mask?.height ?? image.height ?? 1,
1982
+ prompts: structuredClone(prompts)
1983
+ },
1984
+ { signal: controller.signal, onProgress: options.onProgress }
1985
+ );
1986
+ if (controller.signal.aborted)
1987
+ throw new MaskRefinementError("cancelled", "Mask refinement cancelled");
1988
+ if (!/^data:image\/(?:png|jpeg|webp);base64,/i.test(result.dataUrl)) {
1989
+ throw new MaskRefinementError(
1990
+ "invalid-result",
1991
+ "Mask refinement must return a base64 PNG, JPEG, or WebP data URL"
1992
+ );
1993
+ }
1994
+ await this.editor.history.transaction(async () => {
1995
+ await this.editor.replaceImageSource(layerId, result.dataUrl);
1996
+ if (layer.meta.mask) layer.meta.mask.revision += 1;
1997
+ });
1998
+ this.attach(layerId);
1999
+ return result;
2000
+ } catch (error) {
2001
+ if (error instanceof MaskRefinementError) throw error;
2002
+ if (controller.signal.aborted) {
2003
+ throw new MaskRefinementError("cancelled", "Mask refinement cancelled", error);
2004
+ }
2005
+ throw new MaskRefinementError("provider", "Mask refinement provider failed", error);
2006
+ } finally {
2007
+ options.signal?.removeEventListener("abort", abort);
2008
+ if (this.refinement === controller) this.refinement = null;
2009
+ }
2010
+ }
2011
+ cancelRefinement() {
2012
+ this.refinement?.abort(new DOMException("Cancelled", "AbortError"));
2013
+ this.refinement = null;
2014
+ }
2015
+ measure() {
2016
+ if (!this.backing) return null;
2017
+ const started = performance.now();
2018
+ this.context?.getImageData(0, 0, 1, 1);
2019
+ const backingBytes = this.backing.width * this.backing.height * 4;
2020
+ const strokeBackupBytes = this.strokeBackup ? backingBytes : 0;
2021
+ const memory = performance;
2022
+ return {
2023
+ width: this.backing.width,
2024
+ height: this.backing.height,
2025
+ backingBytes,
2026
+ strokeBackupBytes,
2027
+ // The backing store and rollback ImageData dominate interactive mask
2028
+ // memory. Encoded historical rasters are reported separately below.
2029
+ estimatedPeakBytes: backingBytes + strokeBackupBytes,
2030
+ historyBytes: this.editor.history.getSnapshotBytes(),
2031
+ interactionLatencyMs: this.lastInteractionLatencyMs,
2032
+ ...typeof memory.memory?.usedJSHeapSize === "number" ? { usedJsHeapBytes: memory.memory.usedJSHeapSize } : {},
2033
+ elapsedMs: performance.now() - started
2034
+ };
2035
+ }
2036
+ dispose() {
2037
+ this.detach();
2038
+ this.disposed = true;
2039
+ }
2040
+ drawDot(point) {
2041
+ const context = this.context;
2042
+ const brush = this.brush;
2043
+ const radius = brush.size / 2;
2044
+ context.save();
2045
+ context.globalCompositeOperation = brush.mode === "subtract" ? "destination-out" : "source-over";
2046
+ const gradient = context.createRadialGradient(
2047
+ point.x,
2048
+ point.y,
2049
+ radius * brush.hardness,
2050
+ point.x,
2051
+ point.y,
2052
+ radius
2053
+ );
2054
+ const color = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
2055
+ gradient.addColorStop(0, color);
2056
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
2057
+ context.fillStyle = gradient;
2058
+ context.beginPath();
2059
+ context.arc(point.x, point.y, radius, 0, Math.PI * 2);
2060
+ context.fill();
2061
+ context.restore();
2062
+ }
2063
+ makeCanvas(width, height) {
2064
+ const canvas = this.editor.canvas.lowerCanvasEl.ownerDocument.createElement("canvas");
2065
+ canvas.width = width;
2066
+ canvas.height = height;
2067
+ return canvas;
2068
+ }
2069
+ attachBacking(layerId, backing) {
2070
+ const context = backing.getContext("2d");
2071
+ if (!context) throw new Error("2D mask context is unavailable");
2072
+ this.layerId = layerId;
2073
+ this.backing = backing;
2074
+ this.context = context;
2075
+ }
2076
+ requireMask(layerId) {
2077
+ const layer = this.editor.layers.get(layerId);
2078
+ if (!layer || layer.type !== "mask") throw new Error(`Mask layer not found: ${layerId}`);
2079
+ return layer;
2080
+ }
2081
+ assertActive() {
2082
+ if (this.disposed) throw new Error("Mask controller has been disposed");
2083
+ }
2084
+ };
2085
+
1018
2086
  // src/editor.ts
1019
2087
  var MIN_ZOOM = 0.1;
1020
2088
  var MAX_ZOOM = 8;
2089
+ function isTaintedCanvasError(error) {
2090
+ return error instanceof DOMException && error.name === "SecurityError" || error instanceof Error && /taint|cross-origin|insecure/i.test(error.message);
2091
+ }
1021
2092
  var CanvasEditor = class {
1022
2093
  canvas;
1023
2094
  layers;
@@ -1027,6 +2098,10 @@ var CanvasEditor = class {
1027
2098
  snapping;
1028
2099
  crop;
1029
2100
  patterns;
2101
+ fonts;
2102
+ licensing;
2103
+ pages;
2104
+ masks;
1030
2105
  fileAdapter;
1031
2106
  imageProvider;
1032
2107
  zoomLevel = 1;
@@ -1035,13 +2110,18 @@ var CanvasEditor = class {
1035
2110
  // transparent while a mockup preview is shown, so this is the source of truth
1036
2111
  // for serialization and export — not the (possibly transient) canvas value.
1037
2112
  designBackground;
2113
+ designBackgroundImage = null;
2114
+ backgroundImageOptions = null;
1038
2115
  constructor(canvasElement, config) {
1039
2116
  this.events = new EventEmitter();
2117
+ this.fonts = new FontRegistry();
2118
+ config.fonts?.forEach((font) => this.fonts.register(font));
2119
+ this.licensing = new LicenseManager(config.license);
1040
2120
  this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
1041
2121
  const widthPx = this.units.toPixels(config.width);
1042
2122
  const heightPx = this.units.toPixels(config.height);
1043
2123
  this.designBackground = config.backgroundColor ?? "#ffffff";
1044
- this.canvas = new import_fabric4.Canvas(canvasElement, {
2124
+ this.canvas = new import_fabric6.Canvas(canvasElement, {
1045
2125
  width: widthPx,
1046
2126
  height: heightPx,
1047
2127
  backgroundColor: this.designBackground,
@@ -1059,21 +2139,79 @@ var CanvasEditor = class {
1059
2139
  },
1060
2140
  events: this.events
1061
2141
  });
2142
+ this.layers.setHistoryCallback(() => this.history.save());
1062
2143
  this.snapping = new SnapManager(this.canvas, this.events);
1063
2144
  this.crop = new CropController(this.canvas, this.history, this.events);
1064
- this.patterns = new PatternManager(this.canvas, this.layers, this.history);
2145
+ this.patterns = new PatternManager(
2146
+ this.canvas,
2147
+ this.layers,
2148
+ this.history,
2149
+ this.events,
2150
+ config.patternSourceResolver
2151
+ );
1065
2152
  this.setupCanvasEvents();
1066
2153
  this.history.saveImmediate();
2154
+ this.pages = new ProjectManager(this);
2155
+ this.masks = new MaskController(this);
1067
2156
  }
1068
2157
  // ─── Layer Operations ────────────────────────────────
1069
2158
  async addImage(url, options) {
1070
- const img = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top", ...options });
1071
- const layer = this.layers.add("image", img);
1072
- this.history.save();
1073
- return layer;
2159
+ try {
2160
+ const img = await import_fabric6.FabricImage.fromURL(
2161
+ url,
2162
+ {},
2163
+ { originX: "left", originY: "top", ...options }
2164
+ );
2165
+ const layer = this.layers.add("image", img);
2166
+ this.history.save();
2167
+ return layer;
2168
+ } catch (error) {
2169
+ this.events.emit("error", { message: "Failed to add image", error });
2170
+ throw error;
2171
+ }
2172
+ }
2173
+ /** Replace an image source without changing its layer identity or visual transform. */
2174
+ async replaceImageSource(layerId, url) {
2175
+ const layer = this.layers.get(layerId);
2176
+ if (!layer || layer.type !== "image" && layer.type !== "mask") {
2177
+ throw new Error(`Image or mask layer not found: ${layerId}`);
2178
+ }
2179
+ if (layer.meta.pattern) {
2180
+ throw new Error("Clear the pattern before replacing the image source");
2181
+ }
2182
+ if (this.crop.activeLayerId() === layerId) this.crop.cancel();
2183
+ const previous = layer.fabricObject;
2184
+ try {
2185
+ const replacement = await import_fabric6.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2186
+ replacement.set({
2187
+ left: previous.left,
2188
+ top: previous.top,
2189
+ originX: previous.originX,
2190
+ originY: previous.originY,
2191
+ width: previous.width,
2192
+ height: previous.height,
2193
+ cropX: previous.cropX,
2194
+ cropY: previous.cropY,
2195
+ scaleX: previous.scaleX,
2196
+ scaleY: previous.scaleY,
2197
+ angle: previous.angle,
2198
+ skewX: previous.skewX,
2199
+ skewY: previous.skewY,
2200
+ flipX: previous.flipX,
2201
+ flipY: previous.flipY
2202
+ });
2203
+ replacement.filters = [...previous.filters];
2204
+ replacement.applyFilters();
2205
+ replacement.setCoords();
2206
+ this.layers.replaceObject(layerId, replacement);
2207
+ return layer;
2208
+ } catch (error) {
2209
+ this.events.emit("error", { message: "Failed to replace image source", error });
2210
+ throw error;
2211
+ }
1074
2212
  }
1075
2213
  addText(text, options) {
1076
- const textbox = new import_fabric4.Textbox(text, {
2214
+ const textbox = new import_fabric6.Textbox(text, {
1077
2215
  fontSize: 32,
1078
2216
  fontFamily: "Arial",
1079
2217
  fill: "#000000",
@@ -1093,27 +2231,45 @@ var CanvasEditor = class {
1093
2231
  this.history.save();
1094
2232
  return layer;
1095
2233
  }
1096
- async addTemplate(template, _params) {
1097
- const { Textbox: TextboxClass } = await import("fabric");
1098
- const placeholder = new TextboxClass(`[Template: ${template.name}]`, {
1099
- fontSize: 24,
1100
- fontFamily: "Arial",
1101
- fill: "#666666",
1102
- width: 300,
1103
- originX: "left",
1104
- originY: "top"
1105
- });
1106
- const layer = this.layers.add(
1107
- "template",
1108
- placeholder,
1109
- template.name
2234
+ async addTemplate(template, params) {
2235
+ const values = {};
2236
+ for (const parameter of template.parameters) {
2237
+ const value = params[parameter.key] ?? parameter.default;
2238
+ if (value === void 0) throw new Error(`Missing template parameter: ${parameter.key}`);
2239
+ if (parameter.type === "number" && !Number.isFinite(Number(value))) {
2240
+ throw new Error(`Invalid number for template parameter: ${parameter.key}`);
2241
+ }
2242
+ if (parameter.type === "color" && !isCssColor(value)) {
2243
+ throw new Error(`Invalid color for template parameter: ${parameter.key}`);
2244
+ }
2245
+ values[parameter.key] = value;
2246
+ }
2247
+ const resolved = sanitizeSvg(
2248
+ template.svg.replace(/\{\{(\w+)(?:[|:]([^}]*))?\}\}/g, (token, key, fallback) => {
2249
+ const value = values[key] ?? fallback;
2250
+ return value === void 0 ? token : escapeXml(value);
2251
+ })
1110
2252
  );
2253
+ const { objects, options } = await (0, import_fabric6.loadSVGFromString)(resolved);
2254
+ const validObjects = objects.filter((object) => object !== null);
2255
+ if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
2256
+ const group = import_fabric6.util.groupSVGElements(validObjects, options);
2257
+ group.set({
2258
+ left: this.canvas.getWidth() / 2,
2259
+ top: this.canvas.getHeight() / 2,
2260
+ originX: "center",
2261
+ originY: "center"
2262
+ });
2263
+ const layer = this.layers.add("template", group, template.name);
1111
2264
  this.history.save();
1112
2265
  return layer;
1113
2266
  }
1114
2267
  removeLayer(id) {
1115
- this.layers.remove(id);
1116
- this.history.save();
2268
+ if (this.crop.activeLayerId() === id) this.crop.cancel();
2269
+ if (this.masks.activeLayerId() === id) {
2270
+ this.masks.detach(id);
2271
+ }
2272
+ if (this.layers.remove(id)) this.history.save();
1117
2273
  }
1118
2274
  selectLayer(id) {
1119
2275
  this.layers.select(id);
@@ -1148,35 +2304,220 @@ var CanvasEditor = class {
1148
2304
  clone.setCoords();
1149
2305
  const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
1150
2306
  copy.meta = structuredClone(layer.meta);
2307
+ copy.visible = layer.visible;
2308
+ copy.locked = layer.locked;
2309
+ copy.opacity = layer.opacity;
2310
+ clone.set({
2311
+ visible: layer.visible,
2312
+ selectable: !layer.locked,
2313
+ evented: !layer.locked,
2314
+ opacity: layer.opacity
2315
+ });
1151
2316
  this.canvas.setActiveObject(clone);
1152
2317
  this.canvas.requestRenderAll();
1153
2318
  this.history.save();
1154
2319
  return copy;
1155
2320
  }
2321
+ applyImageAdjustments(layerId, adjustments) {
2322
+ const layer = this.layers.get(layerId);
2323
+ if (!layer || layer.type !== "image") return false;
2324
+ const image = layer.fabricObject;
2325
+ const previous = layer.meta.imageAdjustments ?? {};
2326
+ const next = { ...previous, ...adjustments };
2327
+ const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
2328
+ image.filters = [
2329
+ new import_fabric6.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
2330
+ new import_fabric6.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
2331
+ new import_fabric6.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
2332
+ new import_fabric6.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
2333
+ ];
2334
+ layer.meta.imageAdjustments = next;
2335
+ image.applyFilters();
2336
+ this.canvas.requestRenderAll();
2337
+ this.history.save();
2338
+ return true;
2339
+ }
2340
+ /** Combine two or more layers into a single editable group layer. */
2341
+ async groupLayers(ids, name = "Group") {
2342
+ const uniqueIds = [...new Set(ids)];
2343
+ const children = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0);
2344
+ if (children.length < 2 || children.length !== uniqueIds.length) return null;
2345
+ return this.history.transaction(() => {
2346
+ const childData = children.map((layer) => structuredClone(layer.toData()));
2347
+ const objects = children.map((layer) => layer.fabricObject);
2348
+ for (const layer of children) this.layers.remove(layer.id);
2349
+ const group = new import_fabric6.Group(objects);
2350
+ const grouped = this.layers.add("group", group, name);
2351
+ grouped.meta.groupChildren = childData;
2352
+ this.layers.select(grouped.id);
2353
+ this.history.save();
2354
+ return grouped;
2355
+ });
2356
+ }
2357
+ /** Restore a group created by groupLayers back to its original layer records. */
2358
+ async ungroupLayer(id) {
2359
+ const grouped = this.layers.get(id);
2360
+ const childData = grouped?.meta.groupChildren;
2361
+ if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
2362
+ const group = grouped.fabricObject;
2363
+ return this.history.transaction(() => {
2364
+ const transform = group.calcTransformMatrix();
2365
+ const objects = group.removeAll();
2366
+ this.layers.remove(id);
2367
+ const restored = objects.map((object, index) => {
2368
+ import_fabric6.util.addTransformToObject(object, transform);
2369
+ object.setCoords();
2370
+ const data = childData[index];
2371
+ const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
2372
+ if (data?.meta) layer.meta = structuredClone(data.meta);
2373
+ if (data && !data.visible) this.layers.setVisibility(layer.id, false);
2374
+ if (data?.locked) this.layers.setLocked(layer.id, true);
2375
+ if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
2376
+ return layer;
2377
+ });
2378
+ this.history.save();
2379
+ this.layers.select(restored[0]?.id ?? null);
2380
+ return restored;
2381
+ });
2382
+ }
1156
2383
  // ─── Serialization ──────────────────────────────────
1157
2384
  toJSON() {
1158
2385
  return serializeEditor(this);
1159
2386
  }
1160
2387
  async fromJSON(state) {
1161
- await deserializeEditor(this, state);
1162
- this.patterns.repinAll();
2388
+ const managedByHistory = this.history.isRestoring();
2389
+ if (!managedByHistory) {
2390
+ this.history.saveImmediate();
2391
+ this.history.pause();
2392
+ }
2393
+ try {
2394
+ await deserializeEditor(this, state);
2395
+ this.patterns.repinAll();
2396
+ } catch (error) {
2397
+ if (!managedByHistory) {
2398
+ this.events.emit("error", { message: "Failed to load editor state", error });
2399
+ }
2400
+ throw error;
2401
+ } finally {
2402
+ if (!managedByHistory) this.history.resume();
2403
+ }
2404
+ if (!managedByHistory) {
2405
+ this.history.clear();
2406
+ this.history.saveImmediate();
2407
+ }
1163
2408
  }
1164
2409
  // ─── Export ──────────────────────────────────────────
1165
2410
  async toPNG(options) {
1166
- this.events.emit("export:start", { format: "png" });
1167
- const blob = await this.withDesignBackground(() => exportPNG(this.canvas, options));
1168
- this.events.emit("export:complete", { format: "png" });
1169
- return blob;
2411
+ return this.toRaster(options?.format ?? "png", options);
2412
+ }
2413
+ async toJPEG(options) {
2414
+ return this.toRaster("jpeg", options);
2415
+ }
2416
+ async toWebP(options) {
2417
+ return this.toRaster("webp", options);
2418
+ }
2419
+ /** Export one layer in document coordinates or at its native image resolution. */
2420
+ async exportLayer(id, options = {}) {
2421
+ const layer = this.layers.get(id);
2422
+ if (!layer) throw new Error(`Layer not found: ${id}`);
2423
+ try {
2424
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric6.FabricImage) {
2425
+ const image = await layer.fabricObject.clone();
2426
+ image.set({
2427
+ left: 0,
2428
+ top: 0,
2429
+ originX: "left",
2430
+ originY: "top",
2431
+ scaleX: 1,
2432
+ scaleY: 1,
2433
+ angle: 0,
2434
+ flipX: false,
2435
+ flipY: false
2436
+ });
2437
+ return await exportIsolatedPNG(this.canvas, [image], {
2438
+ ...options,
2439
+ width: image.width || 1,
2440
+ height: image.height || 1,
2441
+ cloneObjects: false
2442
+ });
2443
+ }
2444
+ return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
2445
+ } catch (error) {
2446
+ this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
2447
+ throw error;
2448
+ }
2449
+ }
2450
+ /** Export only the configured document background, excluding design layers. */
2451
+ async exportBackground(options = {}) {
2452
+ try {
2453
+ return await exportIsolatedPNG(this.canvas, [], {
2454
+ ...options,
2455
+ backgroundColor: this.designBackground,
2456
+ backgroundImage: this.designBackgroundImage
2457
+ });
2458
+ } catch (error) {
2459
+ this.events.emit("error", { message: "Failed to export background", error });
2460
+ throw error;
2461
+ }
2462
+ }
2463
+ async toRaster(format, options) {
2464
+ this.events.emit("export:start", { format });
2465
+ try {
2466
+ await this.fonts.ready();
2467
+ const blob = await this.withDesignBackground(
2468
+ () => exportPNG(this.canvas, { ...options, format })
2469
+ );
2470
+ this.events.emit("export:complete", { format });
2471
+ this.licensing.track(`export:${format}`);
2472
+ return blob;
2473
+ } catch (error) {
2474
+ this.events.emit("error", {
2475
+ message: isTaintedCanvasError(error) ? "Canvas export was blocked by cross-origin image data; load remote images with CORS enabled" : `Failed to export ${format.toUpperCase()}`,
2476
+ error
2477
+ });
2478
+ throw error;
2479
+ }
1170
2480
  }
1171
2481
  toSVG() {
1172
2482
  this.events.emit("export:start", { format: "svg" });
1173
- const svg = this.withDesignBackground(() => exportSVG(this.canvas));
1174
- this.events.emit("export:complete", { format: "svg" });
1175
- return svg;
2483
+ try {
2484
+ const svg = this.withDesignBackground(() => exportSVG(this.canvas));
2485
+ this.events.emit("export:complete", { format: "svg" });
2486
+ this.licensing.track("export:svg");
2487
+ return svg;
2488
+ } catch (error) {
2489
+ this.events.emit("error", { message: "Failed to export SVG", error });
2490
+ throw error;
2491
+ }
2492
+ }
2493
+ async toSVGAsync(options = {}) {
2494
+ await this.fonts.ready();
2495
+ const svg = this.toSVG();
2496
+ if (options.embedFonts === false || this.fonts.getAll().length === 0) return svg;
2497
+ try {
2498
+ const css = await this.fonts.getEmbeddedCss();
2499
+ return svg.replace(/(<svg\b[^>]*>)/i, `$1<defs><style>${css}</style></defs>`);
2500
+ } catch (error) {
2501
+ this.events.emit("error", { message: "Failed to embed fonts in SVG", error });
2502
+ throw error;
2503
+ }
1176
2504
  }
1177
2505
  toDataURL(format = "png", multiplier = 1) {
1178
2506
  return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
1179
2507
  }
2508
+ /** Export the current product-preview composite. Advanced warping is host-defined. */
2509
+ async toMockupImage(options = {}) {
2510
+ if (!this.mockup) throw new Error("No mockup is configured");
2511
+ await this.fonts.ready();
2512
+ try {
2513
+ const blob = await exportMockup(this.canvas, this.mockup, options);
2514
+ this.licensing.track(`export:mockup:${options.format ?? "png"}`);
2515
+ return blob;
2516
+ } catch (error) {
2517
+ this.events.emit("error", { message: "Failed to export mockup", error });
2518
+ throw error;
2519
+ }
2520
+ }
1180
2521
  /**
1181
2522
  * Run an export with the configured design background applied, even when a
1182
2523
  * mockup preview has forced the live canvas transparent — so exports reflect
@@ -1185,11 +2526,14 @@ var CanvasEditor = class {
1185
2526
  withDesignBackground(fn) {
1186
2527
  if (!this.mockup) return fn();
1187
2528
  const previewBg = this.canvas.backgroundColor;
2529
+ const previewImage = this.canvas.backgroundImage;
1188
2530
  this.canvas.backgroundColor = this.designBackground;
2531
+ this.canvas.backgroundImage = this.designBackgroundImage ?? void 0;
1189
2532
  try {
1190
2533
  return fn();
1191
2534
  } finally {
1192
2535
  this.canvas.backgroundColor = previewBg;
2536
+ this.canvas.backgroundImage = previewImage;
1193
2537
  }
1194
2538
  }
1195
2539
  toPrintifyPositioning() {
@@ -1204,7 +2548,8 @@ var CanvasEditor = class {
1204
2548
  for (const layer of imageLayers) {
1205
2549
  const obj = layer.fabricObject;
1206
2550
  const center = obj.getCenterPoint();
1207
- result[layer.name] = {
2551
+ const key = result[layer.name] ? `${layer.name}-${layer.id}` : layer.name;
2552
+ result[key] = {
1208
2553
  x: round2(center.x / canvasW),
1209
2554
  y: round2(center.y / canvasH),
1210
2555
  scale: round2(obj.scaleX ?? 1),
@@ -1213,26 +2558,92 @@ var CanvasEditor = class {
1213
2558
  }
1214
2559
  return result;
1215
2560
  }
2561
+ /** Provider-neutral, ID-stable placement data in 0..1 canvas coordinates. */
2562
+ toNormalizedPositioning() {
2563
+ const canvasWidth = this.canvas.getWidth();
2564
+ const canvasHeight = this.canvas.getHeight();
2565
+ return this.layers.getAll().map((layer) => {
2566
+ const object = layer.fabricObject;
2567
+ const center = object.getCenterPoint();
2568
+ const bounds = object.getBoundingRect();
2569
+ return {
2570
+ layerId: layer.id,
2571
+ name: layer.name,
2572
+ type: layer.type,
2573
+ centerX: round2(center.x / canvasWidth),
2574
+ centerY: round2(center.y / canvasHeight),
2575
+ width: round2(bounds.width / canvasWidth),
2576
+ height: round2(bounds.height / canvasHeight),
2577
+ scaleX: round2(object.scaleX ?? 1),
2578
+ scaleY: round2(object.scaleY ?? 1),
2579
+ angle: round2(object.angle ?? 0)
2580
+ };
2581
+ });
2582
+ }
2583
+ toProviderPositioning(adapter) {
2584
+ return adapter.map(this.toNormalizedPositioning(), {
2585
+ width: this.canvas.getWidth(),
2586
+ height: this.canvas.getHeight()
2587
+ });
2588
+ }
1216
2589
  // ─── File Operations ────────────────────────────────
1217
2590
  async save(filename, format) {
1218
2591
  if (!this.fileAdapter) return void 0;
1219
2592
  let data;
1220
2593
  if (format === "png") {
1221
2594
  data = await this.toPNG();
2595
+ } else if (format === "jpeg") {
2596
+ data = await this.toJPEG();
2597
+ } else if (format === "webp") {
2598
+ data = await this.toWebP();
1222
2599
  } else if (format === "svg") {
1223
- data = this.toSVG();
2600
+ data = await this.toSVGAsync();
1224
2601
  } else {
1225
2602
  data = JSON.stringify(this.toJSON(), null, 2);
1226
2603
  }
1227
- return this.fileAdapter.save(data, filename, format);
2604
+ try {
2605
+ return await this.fileAdapter.save(data, filename, format);
2606
+ } catch (error) {
2607
+ this.events.emit("error", { message: `Failed to save ${format}`, error });
2608
+ throw error;
2609
+ }
1228
2610
  }
1229
2611
  async uploadImage(file) {
1230
- if (!this.imageProvider) return void 0;
1231
- return this.imageProvider.upload(file);
2612
+ if (!this.imageProvider?.upload) return void 0;
2613
+ try {
2614
+ return await this.imageProvider.upload(file);
2615
+ } catch (error) {
2616
+ this.events.emit("error", { message: "Failed to upload image", error });
2617
+ throw error;
2618
+ }
1232
2619
  }
1233
2620
  async browseImages() {
1234
2621
  if (!this.imageProvider?.browse) return null;
1235
- return this.imageProvider.browse();
2622
+ try {
2623
+ return await this.imageProvider.browse();
2624
+ } catch (error) {
2625
+ this.events.emit("error", { message: "Failed to browse images", error });
2626
+ throw error;
2627
+ }
2628
+ }
2629
+ async searchImages(query, options) {
2630
+ if (!this.imageProvider?.search) return null;
2631
+ try {
2632
+ return await this.imageProvider.search(query, options);
2633
+ } catch (error) {
2634
+ this.events.emit("error", { message: "Failed to search images", error });
2635
+ throw error;
2636
+ }
2637
+ }
2638
+ /** Track provider usage, then insert its hotlinked image as a normal layer. */
2639
+ async addProviderImage(image, options) {
2640
+ try {
2641
+ await this.imageProvider?.trackUse?.(image);
2642
+ } catch (error) {
2643
+ this.events.emit("error", { message: "Failed to record image use", error });
2644
+ throw error;
2645
+ }
2646
+ return this.addImage(image.url, options);
1236
2647
  }
1237
2648
  setFileAdapter(adapter) {
1238
2649
  this.fileAdapter = adapter;
@@ -1248,15 +2659,140 @@ var CanvasEditor = class {
1248
2659
  this.canvas.requestRenderAll();
1249
2660
  }
1250
2661
  this.history.save();
2662
+ this.events.emit("canvas:modified", {});
1251
2663
  }
1252
2664
  getDesignBackground() {
1253
2665
  return this.designBackground;
1254
2666
  }
1255
- resize(width, height) {
2667
+ getDesignBackgroundImage() {
2668
+ return this.designBackgroundImage;
2669
+ }
2670
+ getBackgroundImageOptions() {
2671
+ return this.backgroundImageOptions ? { ...this.backgroundImageOptions } : null;
2672
+ }
2673
+ async setBackgroundImage(url, options = {}) {
2674
+ if (url === null) {
2675
+ this.setBackgroundImageObject(null);
2676
+ return;
2677
+ }
2678
+ try {
2679
+ const image = await import_fabric6.FabricImage.fromURL(
2680
+ url,
2681
+ { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
2682
+ { originX: "left", originY: "top" }
2683
+ );
2684
+ const width = image.width || 1;
2685
+ const height = image.height || 1;
2686
+ const canvasWidth = this.canvas.getWidth();
2687
+ const canvasHeight = this.canvas.getHeight();
2688
+ const fit = options.fit ?? "cover";
2689
+ const sx = canvasWidth / width;
2690
+ const sy = canvasHeight / height;
2691
+ const scaleX = fit === "stretch" ? sx : fit === "contain" ? Math.min(sx, sy) : Math.max(sx, sy);
2692
+ const scaleY = fit === "stretch" ? sy : scaleX;
2693
+ image.set({
2694
+ left: (canvasWidth - width * scaleX) / 2,
2695
+ top: (canvasHeight - height * scaleY) / 2,
2696
+ scaleX,
2697
+ scaleY,
2698
+ opacity: clamp(options.opacity ?? 1, 0, 1),
2699
+ selectable: false,
2700
+ evented: false
2701
+ });
2702
+ const serializableOptions = { ...options };
2703
+ delete serializableOptions.signal;
2704
+ this.setBackgroundImageObject(image, true, serializableOptions);
2705
+ } catch (error) {
2706
+ this.events.emit("error", { message: "Failed to set background image", error });
2707
+ throw error;
2708
+ }
2709
+ }
2710
+ /** Used by state restoration and advanced integrations with an existing Fabric object. */
2711
+ setBackgroundImageObject(image, save = true, options = null) {
2712
+ this.designBackgroundImage = image;
2713
+ this.backgroundImageOptions = image ? options : null;
2714
+ this.canvas.backgroundImage = this.mockup ? void 0 : image ?? void 0;
2715
+ this.canvas.requestRenderAll();
2716
+ if (save) this.history.save();
2717
+ this.events.emit("canvas:modified", {});
2718
+ }
2719
+ setTransparentBackground() {
2720
+ this.setBackground("");
2721
+ }
2722
+ resize(width, height, options = {}) {
1256
2723
  const widthPx = this.units.toPixels(width);
1257
2724
  const heightPx = this.units.toPixels(height);
2725
+ const oldWidth = this.canvas.getWidth();
2726
+ const oldHeight = this.canvas.getHeight();
2727
+ const scaleContent = options.scaleContent ?? true;
2728
+ if (scaleContent && oldWidth > 0 && oldHeight > 0) {
2729
+ const sx = widthPx / oldWidth;
2730
+ const sy = heightPx / oldHeight;
2731
+ for (const layer of this.layers.getAll()) {
2732
+ if (layer.meta.pattern) continue;
2733
+ const object = layer.fabricObject;
2734
+ object.set({
2735
+ left: (object.left ?? 0) * sx,
2736
+ top: (object.top ?? 0) * sy,
2737
+ scaleX: (object.scaleX ?? 1) * sx,
2738
+ scaleY: (object.scaleY ?? 1) * sy
2739
+ });
2740
+ object.setCoords();
2741
+ }
2742
+ if (this.designBackgroundImage) {
2743
+ this.designBackgroundImage.set({
2744
+ left: (this.designBackgroundImage.left ?? 0) * sx,
2745
+ top: (this.designBackgroundImage.top ?? 0) * sy,
2746
+ scaleX: (this.designBackgroundImage.scaleX ?? 1) * sx,
2747
+ scaleY: (this.designBackgroundImage.scaleY ?? 1) * sy
2748
+ });
2749
+ this.designBackgroundImage.setCoords();
2750
+ }
2751
+ }
1258
2752
  this.canvas.setDimensions({ width: widthPx, height: heightPx });
2753
+ this.patterns.repinAll();
1259
2754
  this.canvas.requestRenderAll();
2755
+ this.history.save();
2756
+ this.events.emit("canvas:modified", {});
2757
+ }
2758
+ /** Effective source resolution for an image at its current physical size. */
2759
+ getImageDpi(layerId) {
2760
+ const layer = this.layers.get(layerId);
2761
+ if (!layer || layer.type !== "image") return null;
2762
+ const image = layer.fabricObject;
2763
+ const sourcePixels = image.width ?? 0;
2764
+ const displayedPixels = image.getScaledWidth();
2765
+ if (sourcePixels <= 0 || displayedPixels <= 0) return null;
2766
+ return round2(sourcePixels / (displayedPixels / this.units.getDpi()));
2767
+ }
2768
+ validateImageDpi(minimumDpi = 300) {
2769
+ return this.layers.getAll().filter((layer) => layer.type === "image" && !layer.meta.pattern).flatMap((layer) => {
2770
+ const effectiveDpi = this.getImageDpi(layer.id);
2771
+ return effectiveDpi !== null && effectiveDpi < minimumDpi ? [{ layerId: layer.id, layerName: layer.name, effectiveDpi, minimumDpi }] : [];
2772
+ });
2773
+ }
2774
+ /** Render a small data-URL preview without changing document dimensions. */
2775
+ toThumbnail(maxWidth = 320, maxHeight = 320, format = "png") {
2776
+ const multiplier = Math.min(
2777
+ 1,
2778
+ maxWidth / this.canvas.getWidth(),
2779
+ maxHeight / this.canvas.getHeight()
2780
+ );
2781
+ return this.toDataURL(format, multiplier);
2782
+ }
2783
+ bringToFront(id) {
2784
+ return this.layers.reorder(id, this.layers.count() - 1);
2785
+ }
2786
+ sendToBack(id) {
2787
+ return this.layers.reorder(id, 0);
2788
+ }
2789
+ bringForward(id) {
2790
+ const index = this.layers.getAll().findIndex((layer) => layer.id === id);
2791
+ return index >= 0 && this.layers.reorder(id, index + 1);
2792
+ }
2793
+ sendBackward(id) {
2794
+ const index = this.layers.getAll().findIndex((layer) => layer.id === id);
2795
+ return index >= 0 && this.layers.reorder(id, index - 1);
1260
2796
  }
1261
2797
  // ─── Zoom ───────────────────────────────────────────
1262
2798
  //
@@ -1294,6 +2830,20 @@ var CanvasEditor = class {
1294
2830
  const sy = (viewportHeight - padding * 2) / h;
1295
2831
  this.setZoom(Math.min(sx, sy));
1296
2832
  }
2833
+ /** Zoom until the current selection fills the artboard viewport. */
2834
+ zoomToSelection(padding = 24) {
2835
+ const active = this.canvas.getActiveObject();
2836
+ if (!active) return;
2837
+ active.setCoords();
2838
+ const bounds = active.getBoundingRect();
2839
+ if (bounds.width <= 0 || bounds.height <= 0) return;
2840
+ this.setZoom(
2841
+ Math.min(
2842
+ (this.canvas.getWidth() - padding * 2) / bounds.width,
2843
+ (this.canvas.getHeight() - padding * 2) / bounds.height
2844
+ )
2845
+ );
2846
+ }
1297
2847
  // ─── Patterns ───────────────────────────────────────
1298
2848
  applyPattern(layerId, config) {
1299
2849
  return this.patterns.apply(layerId, config);
@@ -1305,8 +2855,10 @@ var CanvasEditor = class {
1305
2855
  setMockup(mockup) {
1306
2856
  this.mockup = mockup;
1307
2857
  this.canvas.backgroundColor = mockup ? "" : this.designBackground;
2858
+ this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
1308
2859
  this.canvas.requestRenderAll();
1309
2860
  this.events.emit("mockup:changed", { mockup });
2861
+ this.history.save();
1310
2862
  }
1311
2863
  clearMockup() {
1312
2864
  this.setMockup(null);
@@ -1316,9 +2868,11 @@ var CanvasEditor = class {
1316
2868
  }
1317
2869
  // ─── Cleanup ────────────────────────────────────────
1318
2870
  dispose() {
2871
+ this.masks.dispose();
1319
2872
  this.snapping.dispose();
1320
2873
  this.crop.dispose();
1321
2874
  this.history.dispose();
2875
+ clearPatternImageCache();
1322
2876
  this.events.removeAllListeners();
1323
2877
  this.canvas.dispose();
1324
2878
  }
@@ -1359,16 +2913,74 @@ var DEFAULT_PATTERN_CONFIG = {
1359
2913
  rotationStepH: 0,
1360
2914
  rotationStepV: 0
1361
2915
  };
2916
+
2917
+ // src/presets.ts
2918
+ var CANVAS_SIZE_PRESETS = [
2919
+ { id: "a4-portrait", name: "A4 portrait", width: 210, height: 297, unit: "mm", dpi: 300 },
2920
+ { id: "a4-landscape", name: "A4 landscape", width: 297, height: 210, unit: "mm", dpi: 300 },
2921
+ { id: "us-letter", name: "US Letter", width: 8.5, height: 11, unit: "in", dpi: 300 },
2922
+ { id: "shirt-front", name: "Garment front", width: 12, height: 16, unit: "in", dpi: 300 },
2923
+ { id: "instagram-square", name: "Social square", width: 1080, height: 1080, unit: "px", dpi: 72 },
2924
+ { id: "story", name: "Story", width: 1080, height: 1920, unit: "px", dpi: 72 }
2925
+ ];
2926
+
2927
+ // src/annotations.ts
2928
+ var AnnotationOverlay = class {
2929
+ items = /* @__PURE__ */ new Map();
2930
+ transform = { zoom: 1, panX: 0, panY: 0, devicePixelRatio: 1 };
2931
+ set(annotation) {
2932
+ this.items.set(annotation.id, structuredClone(annotation));
2933
+ }
2934
+ remove(id) {
2935
+ return this.items.delete(id);
2936
+ }
2937
+ clear() {
2938
+ this.items.clear();
2939
+ }
2940
+ getAll() {
2941
+ return [...this.items.values()].map((item) => structuredClone(item));
2942
+ }
2943
+ setTransform(transform) {
2944
+ if (!Number.isFinite(transform.zoom) || transform.zoom <= 0) {
2945
+ throw new Error("Annotation zoom must be positive");
2946
+ }
2947
+ this.transform = { ...transform, devicePixelRatio: transform.devicePixelRatio ?? 1 };
2948
+ }
2949
+ documentToViewport(point) {
2950
+ return {
2951
+ x: point.x * this.transform.zoom + this.transform.panX,
2952
+ y: point.y * this.transform.zoom + this.transform.panY
2953
+ };
2954
+ }
2955
+ viewportToDocument(point) {
2956
+ return {
2957
+ x: (point.x - this.transform.panX) / this.transform.zoom,
2958
+ y: (point.y - this.transform.panY) / this.transform.zoom
2959
+ };
2960
+ }
2961
+ documentToDevice(point) {
2962
+ const viewport = this.documentToViewport(point);
2963
+ const ratio = this.transform.devicePixelRatio ?? 1;
2964
+ return { x: viewport.x * ratio, y: viewport.y * ratio };
2965
+ }
2966
+ };
1362
2967
  // Annotate the CommonJS export names for ESM import in node:
1363
2968
  0 && (module.exports = {
2969
+ AnnotationOverlay,
2970
+ CANVAS_SIZE_PRESETS,
1364
2971
  CanvasEditor,
1365
2972
  CropController,
1366
2973
  DEFAULT_PATTERN_CONFIG,
1367
2974
  EventEmitter,
2975
+ FontRegistry,
1368
2976
  HistoryManager,
1369
2977
  Layer,
1370
2978
  LayerManager,
2979
+ LicenseManager,
2980
+ MaskController,
2981
+ MaskRefinementError,
1371
2982
  PatternManager,
2983
+ ProjectManager,
1372
2984
  SnapManager,
1373
2985
  UnitConverter,
1374
2986
  applyPatternLocks,
@@ -1376,15 +2988,23 @@ var DEFAULT_PATTERN_CONFIG = {
1376
2988
  captureLocks,
1377
2989
  clamp,
1378
2990
  clearPatternImageCache,
2991
+ computeCoverPlacement,
2992
+ computePrintAreaClip,
1379
2993
  computeTilePositions,
1380
2994
  deserializeEditor,
2995
+ displaceRgba,
1381
2996
  drawTiles,
2997
+ escapeXml,
1382
2998
  exportDataURL,
2999
+ exportMockup,
1383
3000
  exportPNG,
1384
3001
  exportSVG,
1385
3002
  generateId,
3003
+ isCssColor,
3004
+ loadPatternImage,
1386
3005
  restoreLocks,
1387
3006
  round2,
3007
+ sanitizeSvg,
1388
3008
  serializeEditor
1389
3009
  });
1390
3010
  //# sourceMappingURL=index.js.map