@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/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
- const clamped = Math.max(0, Math.min(this.layers.length - 1, newIndex));
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
- layer.opacity = opacity;
231
- layer.fabricObject.opacity = opacity;
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) return;
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
- if (this.undoStack.length > this.maxSize) {
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 state = this.undoStack.pop();
298
- if (!state) return;
299
- this.redoStack.push(this.getState());
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
- await this.restoreState(state);
302
- this.paused = false;
303
- this.emitChanged();
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.pop();
408
+ const state = this.redoStack.at(-1);
308
409
  if (!state) return;
309
- this.undoStack.push(this.getState());
310
410
  this.paused = true;
311
- await this.restoreState(state);
312
- this.paused = false;
313
- this.emitChanged();
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 > 0;
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
- if (this.debounceTimer) {
345
- clearTimeout(this.debounceTimer);
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 loadImage(src) {
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 loadImage(src);
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 = 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
- var VERSION = "1.0.0";
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
- if (state.mockup !== void 0) {
974
- editor.setMockup(state.mockup);
975
- }
976
- for (const serializedLayer of state.layers) {
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
- async function restoreLayer(editor, serialized) {
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 dataUrl = canvas.toDataURL({
1004
- format,
1005
- multiplier,
1006
- quality
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 response = await fetch(dataUrl);
1009
- return response.blob();
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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(this.canvas, this.layers, this.history);
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
- 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;
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, _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
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.layers.remove(id);
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
- await deserializeEditor(this, state);
1162
- this.patterns.repinAll();
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.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;
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
- const svg = this.withDesignBackground(() => exportSVG(this.canvas));
1174
- this.events.emit("export:complete", { format: "svg" });
1175
- return svg;
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.toSVG();
2051
+ data = await this.toSVGAsync();
1224
2052
  } else {
1225
2053
  data = JSON.stringify(this.toJSON(), null, 2);
1226
2054
  }
1227
- return this.fileAdapter.save(data, filename, format);
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
- return this.imageProvider.upload(file);
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
- return this.imageProvider.browse();
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
- resize(width, height) {
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