@overtone-art/canvas-editor-core 0.5.0 → 0.6.0

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
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  applyLayerShadow: () => applyLayerShadow,
54
54
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
55
55
  applySelectionStyle: () => applySelectionStyle,
56
+ buildCurveLinePaths: () => buildCurveLinePaths,
56
57
  buildCurvePathData: () => buildCurvePathData,
57
58
  clamp: () => clamp,
58
59
  clearTextureMaskCache: () => clearTextureMaskCache,
@@ -64,6 +65,7 @@ __export(index_exports, {
64
65
  displaceRgba: () => displaceRgba,
65
66
  drawTiles: () => drawTiles,
66
67
  escapeXml: () => escapeXml,
68
+ explodeTemplateSvg: () => explodeTemplateSvg,
67
69
  exportDataURL: () => exportDataURL,
68
70
  exportMockup: () => exportMockup,
69
71
  exportPNG: () => exportPNG,
@@ -76,6 +78,7 @@ __export(index_exports, {
76
78
  isShapeMaskId: () => isShapeMaskId,
77
79
  isTextureMaskId: () => isTextureMaskId,
78
80
  needsAbsoluteSpace: () => needsAbsoluteSpace,
81
+ normalizeTextCurve: () => normalizeTextCurve,
79
82
  readLayerShadow: () => readLayerShadow,
80
83
  renderTextureMask: () => renderTextureMask,
81
84
  resetTransform: () => resetTransform,
@@ -84,6 +87,8 @@ __export(index_exports, {
84
87
  sanitizeSvg: () => sanitizeSvg,
85
88
  serializeEditor: () => serializeEditor,
86
89
  shapeMaskPathData: () => shapeMaskPathData,
90
+ textPathCurve: () => textPathCurve,
91
+ textPathSpec: () => textPathSpec,
87
92
  toCanvasSpace: () => toCanvasSpace,
88
93
  toHostSpace: () => toHostSpace,
89
94
  unwrapGroup: () => unwrapGroup
@@ -91,7 +96,7 @@ __export(index_exports, {
91
96
  module.exports = __toCommonJS(index_exports);
92
97
 
93
98
  // src/editor.ts
94
- var import_fabric17 = require("fabric");
99
+ var import_fabric19 = require("fabric");
95
100
 
96
101
  // src/events.ts
97
102
  var EventEmitter = class {
@@ -128,6 +133,9 @@ var EventEmitter = class {
128
133
  }
129
134
  };
130
135
 
136
+ // src/layer.ts
137
+ var import_fabric = require("fabric");
138
+
131
139
  // src/utils/id.ts
132
140
  var import_nanoid = require("nanoid");
133
141
  var generateId = () => (0, import_nanoid.nanoid)(12);
@@ -149,6 +157,14 @@ var Layer = class {
149
157
  * the layer's real object stays the one every editor API talks to.
150
158
  */
151
159
  renderProxy;
160
+ /**
161
+ * Layers nested inside this one, in draw order. Only a `group` layer has any:
162
+ * their fabric objects live inside this layer's fabric `Group`, not on the
163
+ * canvas, which is why the manager keeps them off the canvas stack.
164
+ */
165
+ children;
166
+ /** Group this layer sits inside, or null when it is on the canvas stack. */
167
+ parentId;
152
168
  constructor(type, fabricObject, name, id) {
153
169
  this.id = id ?? generateId();
154
170
  this.type = type;
@@ -159,6 +175,8 @@ var Layer = class {
159
175
  this.meta = {};
160
176
  this.fabricObject = fabricObject;
161
177
  this.renderProxy = null;
178
+ this.children = [];
179
+ this.parentId = null;
162
180
  this.fabricObject._layerId = this.id;
163
181
  }
164
182
  hasMeta() {
@@ -172,12 +190,15 @@ var Layer = class {
172
190
  visible: this.visible,
173
191
  locked: this.locked,
174
192
  opacity: this.opacity,
175
- ...this.hasMeta() ? { meta: this.meta } : {}
193
+ ...this.hasMeta() ? { meta: this.meta } : {},
194
+ ...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
176
195
  };
177
196
  }
178
197
  serialize() {
179
198
  return {
180
199
  ...this.toData(),
200
+ // A group's own object already carries its children's geometry, so the
201
+ // child records in `toData` stay identity-only — nothing is duplicated.
181
202
  fabricObject: this.fabricObject.toObject()
182
203
  };
183
204
  }
@@ -203,9 +224,23 @@ var LayerManager = class {
203
224
  return layer;
204
225
  }
205
226
  remove(id) {
227
+ const layer = this.get(id);
228
+ if (!layer) return false;
229
+ if (layer.parentId) {
230
+ const parent = this.get(layer.parentId);
231
+ if (!parent) return false;
232
+ parent.fabricObject.remove(layer.fabricObject);
233
+ parent.children = parent.children.filter((c) => c.id !== id);
234
+ layer.parentId = null;
235
+ this.events.emit("layer:removed", { layerId: id });
236
+ if (parent.children.length < 2) this.dissolve(parent);
237
+ this.emitChanged();
238
+ this.onPropertyChanged?.();
239
+ this.canvas.requestRenderAll();
240
+ return true;
241
+ }
206
242
  const index = this.layers.findIndex((l) => l.id === id);
207
243
  if (index === -1) return false;
208
- const layer = this.layers[index];
209
244
  this.canvas.remove(layer.fabricObject);
210
245
  if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
211
246
  this.layers.splice(index, 1);
@@ -214,6 +249,81 @@ var LayerManager = class {
214
249
  this.onPropertyChanged?.();
215
250
  return true;
216
251
  }
252
+ /**
253
+ * Take a top-level layer off the canvas stack **without** disposing it — it is
254
+ * about to become a group's child, so its object has to stay alive.
255
+ */
256
+ detach(id) {
257
+ const index = this.layers.findIndex((l) => l.id === id);
258
+ if (index === -1) return void 0;
259
+ const [layer] = this.layers.splice(index, 1);
260
+ this.canvas.remove(layer.fabricObject);
261
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
262
+ return layer;
263
+ }
264
+ /** Put a group's children back on the canvas stack and drop the group. */
265
+ explode(group) {
266
+ const children = [...group.children];
267
+ this.dissolve(group);
268
+ this.events.emit("layer:removed", { layerId: group.id });
269
+ this.emitChanged();
270
+ this.onPropertyChanged?.();
271
+ this.canvas.requestRenderAll();
272
+ return children;
273
+ }
274
+ /**
275
+ * Empty a group onto the canvas stack, in the group's own place, and drop the
276
+ * group record. `removeAll` restores each child's absolute transform, so the
277
+ * group's matrix must NOT be re-applied on top of it.
278
+ */
279
+ dissolve(group) {
280
+ const objects = group.fabricObject.removeAll();
281
+ const at = this.layers.findIndex((l) => l.id === group.id);
282
+ const survivors = group.children;
283
+ group.children = [];
284
+ for (const [index, object] of objects.entries()) {
285
+ object.setCoords();
286
+ const child = survivors[index];
287
+ if (!child) continue;
288
+ child.parentId = null;
289
+ this.layers.splice(at + index, 0, child);
290
+ this.canvas.add(object);
291
+ if (child.renderProxy) this.canvas.add(child.renderProxy);
292
+ }
293
+ this.canvas.remove(group.fabricObject);
294
+ this.layers = this.layers.filter((l) => l.id !== group.id);
295
+ this.syncZOrder();
296
+ }
297
+ /**
298
+ * Re-attach child records to an enlivened group. `data[i]` describes
299
+ * `group._objects[i]` — the order fabric serializes and restores them in. A
300
+ * missing record still becomes a child, so a hand-edited state cannot drop
301
+ * artwork off the canvas.
302
+ */
303
+ adoptGroupChildren(group, data) {
304
+ const objects = group.fabricObject.getObjects();
305
+ group.children = objects.map((object, index) => {
306
+ const record = data[index];
307
+ const child = new Layer(record?.type ?? "shape", object, record?.name, record?.id);
308
+ child.parentId = group.id;
309
+ if (record?.meta) child.meta = structuredClone(record.meta);
310
+ if (record && !record.visible) {
311
+ child.visible = false;
312
+ object.visible = false;
313
+ }
314
+ if (record?.locked) {
315
+ child.locked = true;
316
+ object.selectable = false;
317
+ object.evented = false;
318
+ }
319
+ if (record && record.opacity !== 1) {
320
+ child.opacity = record.opacity;
321
+ object.opacity = record.opacity;
322
+ }
323
+ return child;
324
+ });
325
+ this.emitChanged();
326
+ }
217
327
  /** Replace a layer's render object while preserving its immutable ID and panel state. */
218
328
  replaceObject(id, fabricObject) {
219
329
  const layer = this.get(id);
@@ -278,16 +388,32 @@ var LayerManager = class {
278
388
  if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
279
389
  }
280
390
  }
391
+ /** The list a layer lives in: its group's children, or the canvas stack. */
392
+ siblingsOf(layer) {
393
+ if (!layer.parentId) return this.layers;
394
+ return this.get(layer.parentId)?.children ?? this.layers;
395
+ }
281
396
  reorder(id, newIndex) {
282
- const oldIndex = this.layers.findIndex((l) => l.id === id);
397
+ const target = this.get(id);
398
+ if (!target) return false;
399
+ const list = this.siblingsOf(target);
400
+ const oldIndex = list.indexOf(target);
283
401
  if (oldIndex === -1) return false;
284
402
  if (!Number.isFinite(newIndex)) return false;
285
- const clamped = Math.max(0, Math.min(this.layers.length - 1, Math.round(newIndex)));
403
+ const clamped = Math.max(0, Math.min(list.length - 1, Math.round(newIndex)));
286
404
  if (oldIndex === clamped) return false;
287
- const [layer] = this.layers.splice(oldIndex, 1);
288
- this.layers.splice(clamped, 0, layer);
289
- this.syncZOrder();
290
- this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
405
+ const [layer] = list.splice(oldIndex, 1);
406
+ list.splice(clamped, 0, layer);
407
+ if (layer.parentId) {
408
+ const group = this.get(layer.parentId)?.fabricObject;
409
+ if (group) {
410
+ group.remove(layer.fabricObject);
411
+ group.insertAt(clamped, layer.fabricObject);
412
+ }
413
+ } else {
414
+ this.syncZOrder();
415
+ }
416
+ this.events.emit("layer:reordered", { layerIds: list.map((l) => l.id) });
291
417
  this.emitChanged();
292
418
  this.onPropertyChanged?.();
293
419
  return true;
@@ -295,9 +421,12 @@ var LayerManager = class {
295
421
  select(id) {
296
422
  if (id === null) {
297
423
  this.canvas.discardActiveObject();
424
+ this.exitGroups();
298
425
  } else {
299
426
  const layer = this.get(id);
300
427
  if (layer) {
428
+ if (layer.parentId) this.enterGroup(layer.parentId);
429
+ else this.exitGroups();
301
430
  this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
302
431
  }
303
432
  }
@@ -306,16 +435,67 @@ var LayerManager = class {
306
435
  const selected = id && this.get(id) ? [id] : [];
307
436
  this.events.emit("selection:changed", { selected });
308
437
  }
438
+ /** Drill into a group so its children can be selected and clicked. */
439
+ enterGroup(groupId) {
440
+ const parent = this.get(groupId);
441
+ if (!parent) return;
442
+ const group = parent.fabricObject;
443
+ group.subTargetCheck = true;
444
+ group.interactive = true;
445
+ }
446
+ /** Leave every entered group — selecting anything else is done drilling in. */
447
+ exitGroups() {
448
+ for (const layer of this.layers) {
449
+ if (layer.type !== "group") continue;
450
+ const group = layer.fabricObject;
451
+ if (group.interactive) group.interactive = false;
452
+ }
453
+ }
454
+ /**
455
+ * Select several layers at once, as one fabric `ActiveSelection`. Children are
456
+ * skipped: a selection spanning in and out of a group has no single transform.
457
+ */
458
+ selectMany(ids) {
459
+ const layers = ids.flatMap((id) => {
460
+ const layer = this.get(id);
461
+ return layer && !layer.parentId ? [layer] : [];
462
+ });
463
+ if (layers.length === 0) return this.select(null);
464
+ if (layers.length === 1) return this.select(layers[0].id);
465
+ this.canvas.discardActiveObject();
466
+ this.exitGroups();
467
+ const selection = new import_fabric.ActiveSelection(
468
+ layers.map((l) => l.renderProxy ?? l.fabricObject),
469
+ { canvas: this.canvas }
470
+ );
471
+ this.canvas.setActiveObject(selection);
472
+ this.canvas.requestRenderAll();
473
+ this.events.emit("selection:changed", { selected: layers.map((l) => l.id) });
474
+ }
475
+ /** Depth-first over the whole tree, each layer before its children. */
476
+ *walk(layers = this.layers) {
477
+ for (const layer of layers) {
478
+ yield layer;
479
+ if (layer.children.length > 0) yield* this.walk(layer.children);
480
+ }
481
+ }
309
482
  get(id) {
310
- return this.layers.find((l) => l.id === id);
483
+ for (const layer of this.walk()) if (layer.id === id) return layer;
484
+ return void 0;
311
485
  }
486
+ /** Top-level layers only — the canvas stack. Children hang off their group. */
312
487
  getAll() {
313
488
  return [...this.layers];
314
489
  }
490
+ /** Top-level layers, each carrying its children: what the layer panel renders. */
491
+ getTree() {
492
+ return this.layers.map((l) => l.toData());
493
+ }
315
494
  findByObject(obj) {
316
495
  const layerId = obj._layerId;
317
496
  if (layerId) return this.get(layerId);
318
- return this.layers.find((l) => l.fabricObject === obj);
497
+ for (const layer of this.walk()) if (layer.fabricObject === obj) return layer;
498
+ return void 0;
319
499
  }
320
500
  setVisibility(id, visible) {
321
501
  const layer = this.get(id);
@@ -396,6 +576,7 @@ var LayerManager = class {
396
576
  for (const layer of this.layers) {
397
577
  this.canvas.remove(layer.fabricObject);
398
578
  if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
579
+ layer.children = [];
399
580
  }
400
581
  this.layers = [];
401
582
  this.emitChanged();
@@ -410,6 +591,107 @@ var LayerManager = class {
410
591
  }
411
592
  };
412
593
 
594
+ // src/template-explode.ts
595
+ var import_fabric2 = require("fabric");
596
+ var NAME_MAX = 24;
597
+ function isText(object) {
598
+ return typeof object.text === "string";
599
+ }
600
+ function labelsByRun(template, values) {
601
+ const labels = new Map(template.parameters.map((p) => [p.key, p.label || p.key]));
602
+ const byRun = /* @__PURE__ */ new Map();
603
+ for (const match of template.svg.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g)) {
604
+ const token = /\{\{(\w+)(?:[|:]([^}]*))?\}\}/.exec(match[1]);
605
+ if (!token) continue;
606
+ const label = labels.get(token[1]);
607
+ if (!label) continue;
608
+ const resolved = String(values[token[1]] ?? token[2] ?? "").trim();
609
+ if (resolved) byRun.set(resolved, label);
610
+ }
611
+ return byRun;
612
+ }
613
+ function textName(run, labels) {
614
+ const trimmed = run.trim();
615
+ const label = labels.get(trimmed);
616
+ if (label) return label;
617
+ if (!trimmed) return "Text";
618
+ return trimmed.length > NAME_MAX ? `${trimmed.slice(0, NAME_MAX - 1)}\u2026` : trimmed;
619
+ }
620
+ function shapeName(object) {
621
+ const id = object.id;
622
+ if (typeof id === "string" && id) return id;
623
+ const type = object.type || "shape";
624
+ return type.charAt(0).toUpperCase() + type.slice(1);
625
+ }
626
+ function toTextbox(text) {
627
+ const box = new import_fabric2.Textbox(text.text, {
628
+ left: text.left,
629
+ top: text.top,
630
+ originX: text.originX,
631
+ originY: text.originY,
632
+ angle: text.angle,
633
+ scaleX: text.scaleX,
634
+ scaleY: text.scaleY,
635
+ fontSize: text.fontSize,
636
+ fontFamily: text.fontFamily,
637
+ fontWeight: text.fontWeight,
638
+ fontStyle: text.fontStyle,
639
+ textAlign: text.textAlign,
640
+ charSpacing: text.charSpacing,
641
+ lineHeight: text.lineHeight,
642
+ fill: text.fill,
643
+ stroke: text.stroke,
644
+ strokeWidth: text.strokeWidth,
645
+ opacity: text.opacity,
646
+ // A Textbox wraps at its box width; size it to the run the template drew so
647
+ // the authored copy never comes in pre-wrapped.
648
+ width: Math.max(1, (text.width ?? 0) + 2)
649
+ });
650
+ return box;
651
+ }
652
+ function textPathCurve(template, values, runWidth) {
653
+ if (template.id === "circular-text") return { arc: 100, wave: 0 };
654
+ if (template.id === "wave-text") return { arc: 0, wave: 50 };
655
+ const radius = Number(values.radius ?? 200);
656
+ if (!Number.isFinite(radius) || radius <= 0) return { arc: 0, wave: 0 };
657
+ const arc = 100 * runWidth / (Math.PI * radius);
658
+ return { arc: Math.max(-100, Math.min(100, arc)), wave: 0 };
659
+ }
660
+ function textPathSpec(template, values) {
661
+ const content = String(values.text ?? "Text");
662
+ const fontSize = template.id === "circular-text" ? 28 : 40;
663
+ const box = new import_fabric2.Textbox(content, {
664
+ fontSize,
665
+ fontFamily: String(values.font ?? "Arial"),
666
+ fontWeight: "bold",
667
+ fill: String(values.color ?? "#000000"),
668
+ originX: "left",
669
+ originY: "top",
670
+ // Wide enough that the run stays on one line before the curve widens it.
671
+ width: Math.max(1, content.length * fontSize * 0.62)
672
+ });
673
+ return {
674
+ type: "text",
675
+ name: template.parameters.find((p) => p.key === "text")?.label || "Text",
676
+ object: box,
677
+ curve: textPathCurve(template, values, box.width ?? 0)
678
+ };
679
+ }
680
+ async function explodeTemplateSvg(template, resolvedSvg, values) {
681
+ const labels = labelsByRun(template, values);
682
+ const { objects } = await (0, import_fabric2.loadSVGFromString)(resolvedSvg);
683
+ const specs = [];
684
+ for (const object of objects) {
685
+ if (object === null) continue;
686
+ if (isText(object)) {
687
+ specs.push({ type: "text", name: textName(object.text, labels), object: toTextbox(object) });
688
+ continue;
689
+ }
690
+ specs.push({ type: "shape", name: shapeName(object), object });
691
+ }
692
+ return specs;
693
+ }
694
+
413
695
  // src/history.ts
414
696
  var HistoryManager = class _HistoryManager {
415
697
  static ASSET_KEY = "__canvasEditorHistoryAsset";
@@ -797,7 +1079,7 @@ function bestSnap(edges, candidates, threshold) {
797
1079
  }
798
1080
 
799
1081
  // src/crop.ts
800
- var import_fabric = require("fabric");
1082
+ var import_fabric3 = require("fabric");
801
1083
 
802
1084
  // src/utils/clamp.ts
803
1085
  function clamp(v, min, max) {
@@ -837,7 +1119,7 @@ var CropController = class {
837
1119
  if (prevAngle) image.rotate(0);
838
1120
  image.setCoords();
839
1121
  const b = image.getBoundingRect();
840
- const rect = new import_fabric.Rect({
1122
+ const rect = new import_fabric3.Rect({
841
1123
  left: b.left,
842
1124
  top: b.top,
843
1125
  width: b.width,
@@ -930,10 +1212,10 @@ var CropController = class {
930
1212
  var STROKE2 = "#22c55e";
931
1213
 
932
1214
  // src/pattern/pattern-manager.ts
933
- var import_fabric3 = require("fabric");
1215
+ var import_fabric5 = require("fabric");
934
1216
 
935
1217
  // src/pattern/tiled-pattern-object.ts
936
- var import_fabric2 = require("fabric");
1218
+ var import_fabric4 = require("fabric");
937
1219
 
938
1220
  // src/pattern/tile-geometry.ts
939
1221
  var MAX_TILES_PER_AXIS = 200;
@@ -966,8 +1248,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
966
1248
  }
967
1249
  return placements;
968
1250
  }
969
- function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
1251
+ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
970
1252
  const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
1253
+ const drawW = draw && draw.width > 0 ? draw.width : baseW;
1254
+ const drawH = draw && draw.height > 0 ? draw.height : baseH;
971
1255
  ctx.save();
972
1256
  ctx.translate(anchor.x, anchor.y);
973
1257
  ctx.rotate(config.angle * Math.PI / 180);
@@ -975,7 +1259,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
975
1259
  ctx.save();
976
1260
  ctx.translate(tile.x, tile.y);
977
1261
  ctx.rotate(tile.rotation);
978
- ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
1262
+ ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
979
1263
  ctx.restore();
980
1264
  }
981
1265
  ctx.restore();
@@ -997,7 +1281,8 @@ function mod2(n) {
997
1281
  var MAX_SNAPSHOT_PIXELS = 16e6;
998
1282
  var MAX_SNAPSHOT_SCALE = 8;
999
1283
  var SNAPSHOT_SHRINK_FACTOR = 2;
1000
- var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
1284
+ var TILE_BLEED_DEVICE_PX = 2;
1285
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
1001
1286
  static type = "TiledPattern";
1002
1287
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
1003
1288
  source;
@@ -1080,13 +1365,14 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1080
1365
  const { tileW, tileH } = this.liveTile();
1081
1366
  const centre = this.getCenterPoint();
1082
1367
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1083
- return new import_fabric2.Point(centre.x - shift.x, centre.y - shift.y);
1368
+ return new import_fabric4.Point(centre.x - shift.x, centre.y - shift.y);
1084
1369
  }
1085
1370
  _render(ctx) {
1086
1371
  const { width, height } = this.area;
1087
1372
  if (width <= 0 || height <= 0) return;
1088
1373
  const { tileW, tileH } = this.liveTile();
1089
- const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
1374
+ const scale = contextScale(ctx);
1375
+ const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
1090
1376
  if (!snapshot) return;
1091
1377
  const centre = this.getCenterPoint();
1092
1378
  const angle = this.liveAngle();
@@ -1097,7 +1383,17 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1097
1383
  const shift = this.shiftVector(tileW, tileH, angle);
1098
1384
  const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1099
1385
  const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1100
- drawTiles(ctx, snapshot, config, width, height, tileW, tileH, origin);
1386
+ drawTiles(
1387
+ ctx,
1388
+ snapshot,
1389
+ config,
1390
+ width,
1391
+ height,
1392
+ tileW,
1393
+ tileH,
1394
+ origin,
1395
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
1396
+ );
1101
1397
  ctx.restore();
1102
1398
  }
1103
1399
  /** Raster fallback for SVG export — one `<image>` covering the print area. */
@@ -1123,7 +1419,9 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1123
1419
  height,
1124
1420
  tileW,
1125
1421
  tileH,
1126
- { x: centre.x - shift.x, y: centre.y - shift.y }
1422
+ { x: centre.x - shift.x, y: centre.y - shift.y },
1423
+ // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1424
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1127
1425
  );
1128
1426
  return [
1129
1427
  `<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
@@ -1163,18 +1461,64 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1163
1461
  });
1164
1462
  return Promise.resolve(copy);
1165
1463
  }
1464
+ /**
1465
+ * The source's tile footprint: its bounding box, minus a stroke it reserves
1466
+ * room for but never paints.
1467
+ *
1468
+ * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
1469
+ * colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
1470
+ * stroke. Stepping the grid by that box puts a transparent seam between every
1471
+ * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1472
+ * box it is stepped by.
1473
+ */
1474
+ sourceBox() {
1475
+ const rect = this.source.getBoundingRect();
1476
+ if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1477
+ const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
1478
+ const radians = (this.source.angle ?? 0) * Math.PI / 180;
1479
+ const cos = Math.abs(Math.cos(radians));
1480
+ const sin = Math.abs(Math.sin(radians));
1481
+ return {
1482
+ width: dims.x * cos + dims.y * sin,
1483
+ height: dims.x * sin + dims.y * cos
1484
+ };
1485
+ }
1166
1486
  /** The source's on-canvas width, before the tile scale. */
1167
1487
  baseW() {
1168
- return Math.max(1, this.source.getBoundingRect().width);
1488
+ return Math.max(1, this.sourceBox().width);
1489
+ }
1490
+ /** The source's on-canvas height, before the tile scale. */
1491
+ baseH() {
1492
+ return Math.max(1, this.sourceBox().height);
1493
+ }
1494
+ /**
1495
+ * Size to paint the snapshot at, for a tile of `tileW × tileH`.
1496
+ *
1497
+ * The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
1498
+ * raster up to whole pixels and pads it further for a shadow. Painting it into
1499
+ * the tile step would squeeze the artwork inside that padding — every tile
1500
+ * shrinks by up to a pixel and the grid shows transparent seams at zero
1501
+ * spacing. So the bitmap is painted at its own footprint, scaled by the same
1502
+ * factor the tile is, and the step stays the source's box.
1503
+ */
1504
+ drawSize(tileW, tileH, bleed = 0) {
1505
+ const snapshot = this.snapshotEl;
1506
+ if (!snapshot || this.snapshotScale <= 0) {
1507
+ return { width: tileW + bleed, height: tileH + bleed };
1508
+ }
1509
+ return {
1510
+ width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
1511
+ height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
1512
+ };
1169
1513
  }
1170
1514
  /** Tile size from the config alone, ignoring any in-flight gesture. */
1171
1515
  baseTile() {
1172
- const rect = this.source.getBoundingRect();
1516
+ const box = this.sourceBox();
1173
1517
  const scale = Math.max(1, this.config.scale ?? 100) / 100;
1174
1518
  const floor = this.tileFloor();
1175
1519
  return {
1176
- tileW: Math.max(floor, rect.width * scale),
1177
- tileH: Math.max(floor, rect.height * scale)
1520
+ tileW: Math.max(floor, box.width * scale),
1521
+ tileH: Math.max(floor, box.height * scale)
1178
1522
  };
1179
1523
  }
1180
1524
  /** Tile size as drawn right now, including a live scale gesture. */
@@ -1198,11 +1542,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1198
1542
  const radians = angle * Math.PI / 180;
1199
1543
  const cos = Math.cos(radians);
1200
1544
  const sin = Math.sin(radians);
1201
- return new import_fabric2.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1545
+ return new import_fabric4.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1202
1546
  }
1203
1547
  anchorFromOrigin(origin, tileW, tileH, angle) {
1204
1548
  const shift = this.shiftVector(tileW, tileH, angle);
1205
- return new import_fabric2.Point(origin.x + shift.x, origin.y + shift.y);
1549
+ return new import_fabric4.Point(origin.x + shift.x, origin.y + shift.y);
1206
1550
  }
1207
1551
  /**
1208
1552
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1237,6 +1581,12 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1237
1581
  return Math.max(0.05, Math.min(requested, budgeted));
1238
1582
  }
1239
1583
  };
1584
+ function paintsStroke(object) {
1585
+ if (!object.strokeWidth || object.strokeWidth <= 0) return false;
1586
+ const { stroke } = object;
1587
+ if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
1588
+ return stroke !== null && stroke !== void 0;
1589
+ }
1240
1590
  function clampPercent(value) {
1241
1591
  if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1242
1592
  return Math.max(-100, Math.min(100, value));
@@ -1456,7 +1806,7 @@ async function unbakeLegacyLayer(layer, state) {
1456
1806
  cropY: state.original.cropY,
1457
1807
  angle: state.original.angle
1458
1808
  });
1459
- image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
1809
+ image.clipPath = state.originalClip ? (await import_fabric5.util.enlivenObjects([state.originalClip]))[0] : void 0;
1460
1810
  restoreLocks(image, state.originalLocks);
1461
1811
  image.setCoords();
1462
1812
  }
@@ -1474,91 +1824,154 @@ function restoreLocks(obj, locks) {
1474
1824
  }
1475
1825
 
1476
1826
  // src/text-curve.ts
1477
- var import_fabric4 = require("fabric");
1478
- var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
1827
+ var import_fabric6 = require("fabric");
1828
+
1829
+ // src/text-curve-geometry.ts
1830
+ var DEFAULT_TEXT_CURVE = {
1831
+ shape: "arc",
1832
+ arc: 0,
1833
+ wave: 0,
1834
+ waveLength: 4.1,
1835
+ offset: 0,
1836
+ centerOffset: 0
1837
+ };
1479
1838
  var MIN_ARC = 0.5;
1480
1839
  var FULL_CIRCLE_ARC = 99.5;
1481
1840
  var MIN_SWEEP = 0.12;
1482
- var WAVE_PERIOD_EM = 4.1;
1483
1841
  var WAVE_AMPLITUDE_EM = 0.9;
1842
+ var MIN_WAVE_LENGTH = 1;
1843
+ var MAX_WAVE_LENGTH = 12;
1484
1844
  var WAVE_STEP = 6;
1485
- var MEASURE_WIDTH = 1e5;
1486
- var PATH_SLACK = 0.06;
1487
- function isCurvable(object) {
1488
- return !!object && typeof object.text === "string";
1845
+ var MAX_WAVE_STEPS = 4e3;
1846
+ function round(value) {
1847
+ return Math.round(value * 100) / 100;
1489
1848
  }
1490
- function measureText(text) {
1491
- const authored = text.width;
1492
- try {
1493
- text.set({ width: MEASURE_WIDTH });
1494
- text.initDimensions?.();
1495
- return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1496
- } finally {
1497
- if (authored !== void 0) text.set({ width: authored });
1498
- text.initDimensions?.();
1499
- }
1849
+ function normalizeTextCurve(config) {
1850
+ const arc = clamp(config.arc ?? 0, -100, 100);
1851
+ const wave = clamp(config.wave ?? 0, 0, 100);
1852
+ const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
1853
+ return {
1854
+ shape: config.shape ?? inferred,
1855
+ arc,
1856
+ wave,
1857
+ waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
1858
+ offset: clamp(config.offset ?? 0, -100, 100),
1859
+ centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
1860
+ };
1500
1861
  }
1501
- function arcPathData(width, arc) {
1502
- const magnitude = Math.min(100, Math.abs(arc));
1503
- const direction = arc < 0 ? -1 : 1;
1504
- const full = magnitude >= FULL_CIRCLE_ARC;
1505
- const sweep = full ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
1506
- const radius = width / sweep;
1862
+ function bendsBaseline(config) {
1863
+ return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
1864
+ }
1865
+ function arcGeometry(width, config) {
1866
+ const magnitude = Math.min(100, Math.abs(config.arc));
1867
+ const direction = config.arc < 0 ? -1 : 1;
1868
+ const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
1869
+ const baseRadius = width / baseSweep;
1870
+ const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
1871
+ const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
1872
+ return { radius, sweep, direction, baseRadius };
1873
+ }
1874
+ function arcLinePath(width, geometry, radius, rotation) {
1875
+ const { sweep, direction, baseRadius } = geometry;
1876
+ const full = sweep >= Math.PI * 2 - 1e-6;
1507
1877
  const centerX = width / 2;
1508
1878
  const point = (angle) => [
1509
1879
  centerX + radius * Math.sin(angle),
1510
- direction * radius * (1 - Math.cos(angle))
1880
+ direction * (baseRadius - radius * Math.cos(angle))
1511
1881
  ];
1512
1882
  const sweepFlag = direction > 0 ? 1 : 0;
1513
1883
  const format = ([x, y]) => `${round(x)} ${round(y)}`;
1884
+ const r = round(radius);
1514
1885
  if (full) {
1515
- const start2 = point(-Math.PI);
1516
- const top = point(0);
1886
+ const start2 = point(rotation - Math.PI);
1887
+ const opposite = point(rotation);
1517
1888
  return {
1518
1889
  data: [
1519
1890
  `M ${format(start2)}`,
1520
- `A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(top)}`,
1521
- `A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(start2)}`
1891
+ `A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
1892
+ `A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
1522
1893
  ].join(" "),
1523
- length: width
1894
+ length: radius * Math.PI * 2
1524
1895
  };
1525
1896
  }
1526
- const start = point(-sweep / 2);
1527
- const end = point(sweep / 2);
1897
+ const start = point(rotation - sweep / 2);
1898
+ const end = point(rotation + sweep / 2);
1528
1899
  const largeArc = sweep > Math.PI ? 1 : 0;
1529
1900
  return {
1530
- data: `M ${format(start)} A ${round(radius)} ${round(radius)} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
1531
- // radius = width / sweep, so the arc is exactly `width` long.
1532
- length: width
1901
+ data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
1902
+ length: radius * sweep
1533
1903
  };
1534
1904
  }
1535
- function wavePathData(width, fontSize, wave) {
1536
- const amplitude = clamp(wave, 0, 100) / 100 * fontSize * WAVE_AMPLITUDE_EM;
1537
- const period = Math.max(1, fontSize * WAVE_PERIOD_EM);
1538
- const steps = Math.max(2, Math.ceil(width / WAVE_STEP));
1905
+ function waveLinePath(width, fontSize, config, dy) {
1906
+ const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
1907
+ const period = Math.max(1, fontSize * config.waveLength);
1908
+ const phase = config.offset / 100 * Math.PI * 2;
1909
+ const step = Math.min(WAVE_STEP, period / 8);
1910
+ const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
1539
1911
  const commands = [];
1540
1912
  let length = 0;
1541
1913
  let previous = null;
1542
1914
  for (let index = 0; index <= steps; index++) {
1543
1915
  const x = width * index / steps;
1544
- const y = amplitude * Math.sin(x / period * Math.PI * 2);
1916
+ const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
1545
1917
  if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
1546
1918
  previous = [x, y];
1547
1919
  commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
1548
1920
  }
1549
1921
  return { data: commands.join(" "), length };
1550
1922
  }
1551
- function round(value) {
1552
- return Math.round(value * 100) / 100;
1923
+ function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
1924
+ const curve = normalizeTextCurve(config);
1925
+ if (!bendsBaseline(curve)) return null;
1926
+ const lines = Math.max(1, Math.floor(lineCount));
1927
+ const paths = [];
1928
+ if (curve.shape === "wave") {
1929
+ for (let index = 0; index < lines; index++) {
1930
+ paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
1931
+ }
1932
+ return paths;
1933
+ }
1934
+ const geometry = arcGeometry(width, curve);
1935
+ const rotation = curve.offset / 100 * geometry.sweep;
1936
+ for (let index = 0; index < lines; index++) {
1937
+ const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
1938
+ paths.push(arcLinePath(width, geometry, radius, rotation));
1939
+ }
1940
+ return paths;
1553
1941
  }
1554
1942
  function buildCurvePathData(config, width, fontSize) {
1555
- if (Math.abs(config.arc) >= MIN_ARC) return arcPathData(width, config.arc);
1556
- if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
1557
- return null;
1943
+ return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
1558
1944
  }
1559
- function normalize(config) {
1560
- const arc = clamp(config.arc ?? 0, -100, 100);
1561
- return { arc, wave: Math.abs(arc) >= MIN_ARC ? 0 : clamp(config.wave ?? 0, 0, 100) };
1945
+
1946
+ // src/text-curve.ts
1947
+ var MEASURE_WIDTH = 1e5;
1948
+ var PATH_SLACK = 0.06;
1949
+ var FALLBACK_LINE_HEIGHT = 1.16;
1950
+ var patches = /* @__PURE__ */ new WeakMap();
1951
+ function isCurvable(object) {
1952
+ return !!object && typeof object.text === "string";
1953
+ }
1954
+ function measureText(text) {
1955
+ const authored = text.width;
1956
+ try {
1957
+ text.set({ width: MEASURE_WIDTH });
1958
+ text.initDimensions?.();
1959
+ return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1960
+ } finally {
1961
+ if (authored !== void 0) text.set({ width: authored });
1962
+ text.initDimensions?.();
1963
+ }
1964
+ }
1965
+ function lineCountOf(text) {
1966
+ return Math.max(1, text._textLines?.length ?? 1);
1967
+ }
1968
+ function lineHeightOf(text) {
1969
+ const measured = text.getHeightOfLine?.(0);
1970
+ if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
1971
+ return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
1972
+ }
1973
+ function toFabricPath(curve) {
1974
+ return new import_fabric6.Path(curve.data, { visible: false, objectCaching: false });
1562
1975
  }
1563
1976
  var TextCurveManager = class {
1564
1977
  constructor(canvas, layers, history, events) {
@@ -1575,33 +1988,35 @@ var TextCurveManager = class {
1575
1988
  get(layerId) {
1576
1989
  const layer = this.layers.get(layerId);
1577
1990
  if (!layer || !isCurvable(layer.fabricObject)) return null;
1578
- return layer.meta.curve ?? { ...DEFAULT_TEXT_CURVE };
1991
+ return normalizeTextCurve(layer.meta.curve ?? {});
1579
1992
  }
1580
1993
  isCurved(layerId) {
1581
1994
  const curve = this.get(layerId);
1582
- return !!curve && (Math.abs(curve.arc) >= MIN_ARC || curve.wave > 0);
1995
+ return !!curve && bendsBaseline(curve);
1583
1996
  }
1584
- /** Apply (or update) the curve on a text layer. Zeroed config clears it. */
1997
+ /** Apply (or update) the curve on a text layer. A straight config clears it. */
1585
1998
  apply(layerId, config, save = true) {
1586
1999
  const layer = this.layers.get(layerId);
1587
2000
  if (!layer || !isCurvable(layer.fabricObject)) return false;
1588
- const next = normalize(config);
2001
+ const next = normalizeTextCurve(config);
1589
2002
  const text = layer.fabricObject;
1590
2003
  const run = measureText(text);
1591
- const curve = buildCurvePathData(next, run * (1 + PATH_SLACK), text.fontSize);
1592
- if (!curve) {
2004
+ if (!bendsBaseline(next)) {
1593
2005
  this.detach(text, layer.meta.curveWidth);
1594
- delete layer.meta.curve;
1595
2006
  delete layer.meta.curveWidth;
2007
+ layer.meta.curve = next;
1596
2008
  } else {
1597
2009
  if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
1598
2010
  text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
1599
- text.set({
1600
- path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
1601
- pathAlign: "center",
1602
- pathSide: "left",
1603
- pathStartOffset: Math.max(0, (curve.length - run) / 2)
1604
- });
2011
+ text.initDimensions?.();
2012
+ const curves = buildCurveLinePaths(
2013
+ next,
2014
+ run * (1 + PATH_SLACK),
2015
+ text.fontSize,
2016
+ lineCountOf(text),
2017
+ lineHeightOf(text)
2018
+ );
2019
+ if (curves) this.attach(text, curves);
1605
2020
  layer.meta.curve = next;
1606
2021
  }
1607
2022
  text.initDimensions?.();
@@ -1614,12 +2029,14 @@ var TextCurveManager = class {
1614
2029
  }
1615
2030
  /** Remove the curve, restoring the authored text box width. */
1616
2031
  clear(layerId, save = true) {
1617
- return this.apply(layerId, DEFAULT_TEXT_CURVE, save);
2032
+ const layer = this.layers.get(layerId);
2033
+ const shape = layer?.meta.curve?.shape;
2034
+ return this.apply(layerId, { shape }, save);
1618
2035
  }
1619
2036
  /**
1620
- * Rebuild the path from the stored parameters. Text content, font family and
1621
- * font size all change the run's width, and the path is sized to that width
1622
- * without this the curve keeps the geometry of the text it was created from.
2037
+ * Rebuild the paths from the stored parameters. Text content, line count,
2038
+ * font family and font size all change the geometry the paths are sized to —
2039
+ * without this the curve keeps that of the text it was created from.
1623
2040
  */
1624
2041
  refresh(layerId, save = false) {
1625
2042
  const curve = this.layers.get(layerId)?.meta.curve;
@@ -1632,14 +2049,84 @@ var TextCurveManager = class {
1632
2049
  if (layer.meta.curve) this.refresh(layer.id);
1633
2050
  }
1634
2051
  }
2052
+ /**
2053
+ * Puts every line's path on the object as one shape, and hooks per-line
2054
+ * measuring so each line is laid out on its own.
2055
+ *
2056
+ * The object's own path is the union of the lines: fabric sizes a curved text
2057
+ * object from it, and a single line's path would leave the rest of the block
2058
+ * outside the layer's box — where object caching clips it away.
2059
+ */
2060
+ attach(text, curves) {
2061
+ const paths = curves.map(toFabricPath);
2062
+ const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
2063
+ text.set({
2064
+ path: union,
2065
+ pathAlign: "center",
2066
+ pathSide: "left",
2067
+ // Each line is centred on its own path by the measuring hook below, so
2068
+ // there is no single offset left to apply.
2069
+ pathStartOffset: 0
2070
+ });
2071
+ this.patchLineMeasure(text, paths, union);
2072
+ }
2073
+ /**
2074
+ * Fabric lays every line of a text object along `this.path`, from one
2075
+ * `pathStartOffset` — so a multi-line run piles all its lines on top of each
2076
+ * other. Each line is measured against its own path instead, by swapping the
2077
+ * path in around fabric's own per-line measure pass.
2078
+ *
2079
+ * The paths all share the first one's `pathOffset`, which is the origin
2080
+ * fabric subtracts to place a glyph in the object — without that each line
2081
+ * would be re-centred on its own bounding box and land back in the same spot.
2082
+ *
2083
+ * `textAlign` is switched to centre for the pass, which is how fabric centres
2084
+ * a line on its path; the authored value is restored before anything else
2085
+ * reads it.
2086
+ */
2087
+ patchLineMeasure(text, paths, union) {
2088
+ this.unpatchLineMeasure(text);
2089
+ const original = text._measureLine;
2090
+ if (typeof original !== "function") return;
2091
+ for (const path of paths) path.pathOffset = union.pathOffset;
2092
+ const installed = function(lineIndex) {
2093
+ const path = paths[lineIndex];
2094
+ if (!path) return original.call(this, lineIndex);
2095
+ const previousPath = this.path;
2096
+ const previousAlign = this.textAlign;
2097
+ this.path = path;
2098
+ this.textAlign = "center";
2099
+ this.setPathInfo?.();
2100
+ try {
2101
+ return original.call(this, lineIndex);
2102
+ } finally {
2103
+ this.path = previousPath;
2104
+ this.textAlign = previousAlign;
2105
+ }
2106
+ };
2107
+ const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "_measureLine");
2108
+ text._measureLine = installed;
2109
+ patches.set(text, { original, installed, ownedOriginal });
2110
+ }
2111
+ /** Restore fabric's own per-line measuring, if this manager replaced it. */
2112
+ unpatchLineMeasure(text) {
2113
+ const patch = patches.get(text);
2114
+ if (!patch) return;
2115
+ if (text._measureLine === patch.installed) {
2116
+ if (patch.ownedOriginal) text._measureLine = patch.original;
2117
+ else delete text._measureLine;
2118
+ }
2119
+ patches.delete(text);
2120
+ }
1635
2121
  detach(text, authoredWidth) {
2122
+ this.unpatchLineMeasure(text);
1636
2123
  text.set({ path: null, pathStartOffset: 0 });
1637
2124
  if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
1638
2125
  }
1639
2126
  };
1640
2127
 
1641
2128
  // src/mask-presets/manager.ts
1642
- var import_fabric5 = require("fabric");
2129
+ var import_fabric7 = require("fabric");
1643
2130
 
1644
2131
  // src/mask-presets/shapes.ts
1645
2132
  var SHAPE_MASK_IDS = [
@@ -1906,13 +2393,13 @@ var MaskPresetManager = class {
1906
2393
  objectCaching: false
1907
2394
  };
1908
2395
  if (isShapeMaskId(id)) {
1909
- return new import_fabric5.Path(shapeMaskPathData(id), {
2396
+ return new import_fabric7.Path(shapeMaskPathData(id), {
1910
2397
  ...shared,
1911
2398
  scaleX: width / SHAPE_MASK_BOX,
1912
2399
  scaleY: height / SHAPE_MASK_BOX
1913
2400
  });
1914
2401
  }
1915
- return new import_fabric5.FabricImage(renderTextureMask(id), {
2402
+ return new import_fabric7.FabricImage(renderTextureMask(id), {
1916
2403
  ...shared,
1917
2404
  scaleX: width / TEXTURE_MASK_SIZE,
1918
2405
  scaleY: height / TEXTURE_MASK_SIZE
@@ -1921,7 +2408,7 @@ var MaskPresetManager = class {
1921
2408
  };
1922
2409
 
1923
2410
  // src/shadow.ts
1924
- var import_fabric6 = require("fabric");
2411
+ var import_fabric8 = require("fabric");
1925
2412
 
1926
2413
  // src/utils/color.ts
1927
2414
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -1959,7 +2446,7 @@ function applyLayerShadow(object, config) {
1959
2446
  }
1960
2447
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
1961
2448
  object.set({
1962
- shadow: new import_fabric6.Shadow({
2449
+ shadow: new import_fabric8.Shadow({
1963
2450
  color,
1964
2451
  blur: Math.max(0, next.blur),
1965
2452
  offsetX: next.offsetX,
@@ -2073,7 +2560,7 @@ var UnitConverter = class {
2073
2560
  };
2074
2561
 
2075
2562
  // src/serialization.ts
2076
- var import_fabric7 = require("fabric");
2563
+ var import_fabric9 = require("fabric");
2077
2564
  var VERSION = "2.0.0";
2078
2565
  function serializeEditor(editor) {
2079
2566
  return {
@@ -2109,7 +2596,7 @@ async function deserializeEditor(editor, state) {
2109
2596
  }
2110
2597
  const staged = await Promise.all(
2111
2598
  state.layers.map(async (serialized) => {
2112
- const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
2599
+ const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
2113
2600
  if (!fabricObject) {
2114
2601
  const source = serialized.fabricObject.src;
2115
2602
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2120,7 +2607,7 @@ async function deserializeEditor(editor, state) {
2120
2607
  return { serialized, fabricObject };
2121
2608
  })
2122
2609
  );
2123
- const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
2610
+ const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
2124
2611
  if (state.backgroundImage && !stagedBackground) {
2125
2612
  const source = state.backgroundImage.src;
2126
2613
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2163,11 +2650,16 @@ function restoreLayer(editor, serialized, fabricObject) {
2163
2650
  if (serialized.opacity !== 1) {
2164
2651
  editor.layers.setOpacity(layer.id, serialized.opacity);
2165
2652
  }
2653
+ const childData = serialized.children ?? serialized.meta?.groupChildren;
2654
+ if (layer.type === "group" && Array.isArray(childData)) {
2655
+ editor.layers.adoptGroupChildren(layer, childData);
2656
+ if (layer.meta.groupChildren) delete layer.meta.groupChildren;
2657
+ }
2166
2658
  return layer;
2167
2659
  }
2168
2660
 
2169
2661
  // src/export.ts
2170
- var import_fabric8 = require("fabric");
2662
+ var import_fabric10 = require("fabric");
2171
2663
 
2172
2664
  // src/displacement.ts
2173
2665
  var CHANNEL_INDEX = {
@@ -2261,7 +2753,7 @@ async function exportPNG(canvas, options = {}) {
2261
2753
  }
2262
2754
  async function exportIsolatedPNG(source, objects, options = {}) {
2263
2755
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2264
- const canvas = new import_fabric8.StaticCanvas(element, {
2756
+ const canvas = new import_fabric10.StaticCanvas(element, {
2265
2757
  width: options.width ?? source.getWidth(),
2266
2758
  height: options.height ?? source.getHeight(),
2267
2759
  backgroundColor: options.backgroundColor || void 0
@@ -2291,7 +2783,7 @@ async function exportPrintArea(source, area, options = {}) {
2291
2783
  throw new Error("Print area does not overlap the canvas");
2292
2784
  }
2293
2785
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2294
- const canvas = new import_fabric8.StaticCanvas(element, { width, height });
2786
+ const canvas = new import_fabric10.StaticCanvas(element, { width, height });
2295
2787
  try {
2296
2788
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2297
2789
  if (clones.length) canvas.add(...clones);
@@ -2731,7 +3223,7 @@ var ProjectManager = class {
2731
3223
  };
2732
3224
 
2733
3225
  // src/mask.ts
2734
- var import_fabric9 = require("fabric");
3226
+ var import_fabric11 = require("fabric");
2735
3227
  var MaskRefinementError = class extends Error {
2736
3228
  constructor(code, message, cause) {
2737
3229
  super(message);
@@ -2763,7 +3255,7 @@ var MaskController = class {
2763
3255
  throw new Error("Mask dimensions must be positive integers");
2764
3256
  }
2765
3257
  const backing = this.makeCanvas(width, height);
2766
- const image = new import_fabric9.FabricImage(backing, {
3258
+ const image = new import_fabric11.FabricImage(backing, {
2767
3259
  left: 0,
2768
3260
  top: 0,
2769
3261
  originX: "left",
@@ -2989,10 +3481,10 @@ var MaskController = class {
2989
3481
  };
2990
3482
 
2991
3483
  // src/masks/manager.ts
2992
- var import_fabric16 = require("fabric");
3484
+ var import_fabric18 = require("fabric");
2993
3485
 
2994
3486
  // src/masks/compose.ts
2995
- var import_fabric10 = require("fabric");
3487
+ var import_fabric12 = require("fabric");
2996
3488
  var MODE_OPERATION = {
2997
3489
  add: "source-over",
2998
3490
  subtract: "destination-out",
@@ -3002,7 +3494,7 @@ function neutralize(child) {
3002
3494
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3003
3495
  }
3004
3496
  function baseRect(box) {
3005
- return new import_fabric10.Rect({
3497
+ return new import_fabric12.Rect({
3006
3498
  left: box.left,
3007
3499
  top: box.top,
3008
3500
  width: Math.max(1, box.width),
@@ -3029,7 +3521,7 @@ function composeMaskGroup(children, entries, options) {
3029
3521
  });
3030
3522
  const first = entries.find((entry) => entry.visible);
3031
3523
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3032
- return new import_fabric10.Group(withBase, {
3524
+ return new import_fabric12.Group(withBase, {
3033
3525
  absolutePositioned: options.absolute,
3034
3526
  // Cached, so the children's compositing operations resolve against each
3035
3527
  // other instead of against the page underneath the mask.
@@ -3043,15 +3535,15 @@ function needsAbsoluteSpace(entries) {
3043
3535
  }
3044
3536
 
3045
3537
  // src/masks/edit.ts
3046
- var import_fabric12 = require("fabric");
3538
+ var import_fabric14 = require("fabric");
3047
3539
 
3048
3540
  // src/masks/space.ts
3049
- var import_fabric11 = require("fabric");
3541
+ var import_fabric13 = require("fabric");
3050
3542
  function matrixOf(object) {
3051
3543
  return object.calcTransformMatrix();
3052
3544
  }
3053
3545
  function applyMatrix(object, matrix) {
3054
- const decomposed = import_fabric11.util.qrDecompose(matrix);
3546
+ const decomposed = import_fabric13.util.qrDecompose(matrix);
3055
3547
  object.set({
3056
3548
  flipX: false,
3057
3549
  flipY: false,
@@ -3068,19 +3560,19 @@ function applyMatrix(object, matrix) {
3068
3560
  object.setCoords();
3069
3561
  }
3070
3562
  function toCanvasSpace(object, host) {
3071
- applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3563
+ applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3072
3564
  }
3073
3565
  function toHostSpace(object, host) {
3074
3566
  applyMatrix(
3075
3567
  object,
3076
- import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object))
3568
+ import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
3077
3569
  );
3078
3570
  }
3079
3571
  function relativeMatrix(object, host) {
3080
- return import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object));
3572
+ return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
3081
3573
  }
3082
3574
  function applyRelativeMatrix(object, host, rel) {
3083
- applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), rel));
3575
+ applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
3084
3576
  }
3085
3577
  function asObject(clip) {
3086
3578
  return clip;
@@ -3193,8 +3685,8 @@ var MaskEditController = class {
3193
3685
  if (!this.handle || !this.child || !this.group) return;
3194
3686
  applyMatrix(
3195
3687
  this.child,
3196
- import_fabric12.util.multiplyTransformMatrices(
3197
- import_fabric12.util.invertTransform(matrixOf(this.group)),
3688
+ import_fabric14.util.multiplyTransformMatrices(
3689
+ import_fabric14.util.invertTransform(matrixOf(this.group)),
3198
3690
  matrixOf(this.handle)
3199
3691
  )
3200
3692
  );
@@ -3205,15 +3697,15 @@ var MaskEditController = class {
3205
3697
  };
3206
3698
 
3207
3699
  // src/masks/store.ts
3208
- var import_fabric15 = require("fabric");
3700
+ var import_fabric17 = require("fabric");
3209
3701
 
3210
3702
  // src/masks/host.ts
3211
- var import_fabric13 = require("fabric");
3703
+ var import_fabric15 = require("fabric");
3212
3704
  function findCanvasHost(layers) {
3213
3705
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3214
3706
  }
3215
3707
  function createCanvasHost(canvas, layers) {
3216
- const rect = new import_fabric13.Rect({
3708
+ const rect = new import_fabric15.Rect({
3217
3709
  left: 0,
3218
3710
  top: 0,
3219
3711
  width: canvas.getWidth(),
@@ -3262,9 +3754,9 @@ function hostBoxOf(canvas, host, absolute) {
3262
3754
  }
3263
3755
 
3264
3756
  // src/masks/install.ts
3265
- var import_fabric14 = require("fabric");
3757
+ var import_fabric16 = require("fabric");
3266
3758
  function convertSpace(host, sources, absolute) {
3267
- const wasAbsolute = host.clipPath instanceof import_fabric14.Group ? host.clipPath.absolutePositioned : absolute;
3759
+ const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
3268
3760
  if (absolute === wasAbsolute) return;
3269
3761
  for (const source of sources) {
3270
3762
  if (absolute) toCanvasSpace(source, host);
@@ -3356,7 +3848,7 @@ var MaskStackStore = class {
3356
3848
  const clip = host.clipPath;
3357
3849
  if (!clip) return [];
3358
3850
  const entries = this.list(target);
3359
- if (entries.length === 0 || !(clip instanceof import_fabric15.Group)) return [asObject(clip)];
3851
+ if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
3360
3852
  const children = unwrapGroup(clip);
3361
3853
  const extra = children.length - entries.length;
3362
3854
  return extra > 0 ? children.slice(extra) : children;
@@ -3456,7 +3948,7 @@ var LayerMaskManager = class extends MaskStackStore {
3456
3948
  if (index === -1) return false;
3457
3949
  this.endEdit(false);
3458
3950
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
3459
- const group = host.clipPath instanceof import_fabric16.Group ? host.clipPath : null;
3951
+ const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
3460
3952
  if (!group) return false;
3461
3953
  const children = group.getObjects();
3462
3954
  const child = children[children.length - entries.length + index];
@@ -3762,7 +4254,7 @@ var CanvasEditor = class {
3762
4254
  const widthPx = this.units.toPixels(config.width);
3763
4255
  const heightPx = this.units.toPixels(config.height);
3764
4256
  this.designBackground = config.backgroundColor ?? "#ffffff";
3765
- this.canvas = new import_fabric17.Canvas(canvasElement, {
4257
+ this.canvas = new import_fabric19.Canvas(canvasElement, {
3766
4258
  width: widthPx,
3767
4259
  height: heightPx,
3768
4260
  backgroundColor: this.designBackground,
@@ -3796,7 +4288,7 @@ var CanvasEditor = class {
3796
4288
  // ─── Layer Operations ────────────────────────────────
3797
4289
  async addImage(url, options) {
3798
4290
  try {
3799
- const img = await import_fabric17.FabricImage.fromURL(
4291
+ const img = await import_fabric19.FabricImage.fromURL(
3800
4292
  url,
3801
4293
  {},
3802
4294
  { originX: "left", originY: "top", ...options }
@@ -3822,7 +4314,7 @@ var CanvasEditor = class {
3822
4314
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
3823
4315
  const previous = layer.fabricObject;
3824
4316
  try {
3825
- const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4317
+ const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
3826
4318
  replacement.set({
3827
4319
  left: previous.left,
3828
4320
  top: previous.top,
@@ -3851,7 +4343,7 @@ var CanvasEditor = class {
3851
4343
  }
3852
4344
  }
3853
4345
  addText(text, options) {
3854
- const textbox = new import_fabric17.Textbox(text, {
4346
+ const textbox = new import_fabric19.Textbox(text, {
3855
4347
  fontSize: 32,
3856
4348
  fontFamily: "Arial",
3857
4349
  fill: "#000000",
@@ -3892,20 +4384,30 @@ var CanvasEditor = class {
3892
4384
  return value === void 0 ? token : escapeXml(value);
3893
4385
  })
3894
4386
  );
3895
- const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
3896
- const validObjects = objects.filter((object) => object !== null);
3897
- if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
3898
- const group = import_fabric17.util.groupSVGElements(validObjects, options);
3899
- group.set({
3900
- left: this.canvas.getWidth() / 2,
3901
- top: this.canvas.getHeight() / 2,
3902
- originX: "center",
3903
- originY: "center"
4387
+ const specs = resolved.includes("<textPath") ? [textPathSpec(template, values)] : await explodeTemplateSvg(template, resolved, values);
4388
+ if (specs.length === 0) throw new Error("Template SVG contains no renderable objects");
4389
+ return this.history.transaction(async () => {
4390
+ const created = specs.map((spec) => {
4391
+ const layer2 = this.layers.add(spec.type, spec.object, spec.name);
4392
+ if (spec.curve) this.curves.apply(layer2.id, spec.curve, false);
4393
+ return layer2;
4394
+ });
4395
+ const layer = created.length === 1 ? created[0] : await this.groupLayers(
4396
+ created.map((l) => l.id),
4397
+ template.name
4398
+ ) ?? created[0];
4399
+ layer.fabricObject.set({
4400
+ left: this.canvas.getWidth() / 2,
4401
+ top: this.canvas.getHeight() / 2,
4402
+ originX: "center",
4403
+ originY: "center"
4404
+ });
4405
+ layer.fabricObject.setCoords();
4406
+ this.layers.select(layer.id);
4407
+ this.canvas.requestRenderAll();
4408
+ this.history.save();
4409
+ return layer;
3904
4410
  });
3905
- const layer = this.layers.add("template", group, template.name);
3906
- this.layers.select(layer.id);
3907
- this.history.save();
3908
- return layer;
3909
4411
  }
3910
4412
  removeLayer(id) {
3911
4413
  if (this.crop.activeLayerId() === id) this.crop.cancel();
@@ -3917,6 +4419,10 @@ var CanvasEditor = class {
3917
4419
  selectLayer(id) {
3918
4420
  this.layers.select(id);
3919
4421
  }
4422
+ /** Select several layers at once — what a panel's shift/⌘ click drives. */
4423
+ selectLayers(ids) {
4424
+ this.layers.selectMany(ids);
4425
+ }
3920
4426
  getSelectedLayer() {
3921
4427
  const active = this.canvas.getActiveObject();
3922
4428
  if (!active) return null;
@@ -3968,10 +4474,10 @@ var CanvasEditor = class {
3968
4474
  const next = { ...previous, ...adjustments };
3969
4475
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
3970
4476
  image.filters = [
3971
- new import_fabric17.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
3972
- new import_fabric17.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
3973
- new import_fabric17.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
3974
- new import_fabric17.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4477
+ new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4478
+ new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4479
+ new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4480
+ new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
3975
4481
  ];
3976
4482
  layer.meta.imageAdjustments = next;
3977
4483
  image.applyFilters();
@@ -3979,42 +4485,55 @@ var CanvasEditor = class {
3979
4485
  this.history.save();
3980
4486
  return true;
3981
4487
  }
3982
- /** Combine two or more layers into a single editable group layer. */
4488
+ /**
4489
+ * Combine two or more layers into one group layer. Picking a group among them
4490
+ * **absorbs** it — its children join the new group and the old group record is
4491
+ * dropped — because groups are exactly one level deep.
4492
+ */
3983
4493
  async groupLayers(ids, name = "Group") {
3984
4494
  const uniqueIds = [...new Set(ids)];
3985
- const children = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0);
3986
- if (children.length < 2 || children.length !== uniqueIds.length) return null;
4495
+ const picked = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0 && layer.parentId === null);
4496
+ if (picked.length < 2 || picked.length !== uniqueIds.length) return null;
4497
+ const stack = this.layers.getAll();
4498
+ const topIndex = Math.max(...picked.map((layer) => stack.indexOf(layer)));
4499
+ const insertIndex = stack.slice(0, topIndex).filter((layer) => !picked.includes(layer)).length;
3987
4500
  return this.history.transaction(() => {
3988
- const childData = children.map((layer) => structuredClone(layer.toData()));
3989
- const objects = children.map((layer) => layer.fabricObject);
3990
- for (const layer of children) this.layers.remove(layer.id);
3991
- const group = new import_fabric17.Group(objects);
4501
+ const children = [];
4502
+ const objects = [];
4503
+ for (const layer of picked) {
4504
+ if (layer.type === "group" && layer.children.length > 0) {
4505
+ const inner = layer.fabricObject.removeAll();
4506
+ for (const [index, object] of inner.entries()) {
4507
+ object.setCoords();
4508
+ const child = layer.children[index];
4509
+ if (!child) continue;
4510
+ children.push(child);
4511
+ objects.push(object);
4512
+ }
4513
+ layer.children = [];
4514
+ this.layers.detach(layer.id);
4515
+ } else {
4516
+ children.push(layer);
4517
+ objects.push(layer.fabricObject);
4518
+ this.layers.detach(layer.id);
4519
+ }
4520
+ }
4521
+ const group = new import_fabric19.Group(objects);
3992
4522
  const grouped = this.layers.add("group", group, name);
3993
- grouped.meta.groupChildren = childData;
4523
+ for (const child of children) child.parentId = grouped.id;
4524
+ grouped.children = children;
4525
+ this.layers.reorder(grouped.id, insertIndex);
3994
4526
  this.layers.select(grouped.id);
3995
4527
  this.history.save();
3996
4528
  return grouped;
3997
4529
  });
3998
4530
  }
3999
- /** Restore a group created by groupLayers back to its original layer records. */
4531
+ /** Split a group back into the layers it holds, in place. */
4000
4532
  async ungroupLayer(id) {
4001
4533
  const grouped = this.layers.get(id);
4002
- const childData = grouped?.meta.groupChildren;
4003
- if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
4004
- const group = grouped.fabricObject;
4534
+ if (!grouped || grouped.type !== "group" || grouped.children.length === 0) return [];
4005
4535
  return this.history.transaction(() => {
4006
- const objects = group.removeAll();
4007
- this.layers.remove(id);
4008
- const restored = objects.map((object, index) => {
4009
- object.setCoords();
4010
- const data = childData[index];
4011
- const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
4012
- if (data?.meta) layer.meta = structuredClone(data.meta);
4013
- if (data && !data.visible) this.layers.setVisibility(layer.id, false);
4014
- if (data?.locked) this.layers.setLocked(layer.id, true);
4015
- if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
4016
- return layer;
4017
- });
4536
+ const restored = this.layers.explode(grouped);
4018
4537
  this.history.save();
4019
4538
  this.layers.select(restored[0]?.id ?? null);
4020
4539
  return restored;
@@ -4062,7 +4581,7 @@ var CanvasEditor = class {
4062
4581
  const layer = this.layers.get(id);
4063
4582
  if (!layer) throw new Error(`Layer not found: ${id}`);
4064
4583
  try {
4065
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
4584
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
4066
4585
  const image = await layer.fabricObject.clone();
4067
4586
  image.set({
4068
4587
  left: 0,
@@ -4338,7 +4857,7 @@ var CanvasEditor = class {
4338
4857
  return;
4339
4858
  }
4340
4859
  try {
4341
- const image = await import_fabric17.FabricImage.fromURL(
4860
+ const image = await import_fabric19.FabricImage.fromURL(
4342
4861
  url,
4343
4862
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
4344
4863
  { originX: "left", originY: "top" }
@@ -4762,6 +5281,7 @@ var AnnotationOverlay = class {
4762
5281
  applyLayerShadow,
4763
5282
  applyObjectSelectionStyle,
4764
5283
  applySelectionStyle,
5284
+ buildCurveLinePaths,
4765
5285
  buildCurvePathData,
4766
5286
  clamp,
4767
5287
  clearTextureMaskCache,
@@ -4773,6 +5293,7 @@ var AnnotationOverlay = class {
4773
5293
  displaceRgba,
4774
5294
  drawTiles,
4775
5295
  escapeXml,
5296
+ explodeTemplateSvg,
4776
5297
  exportDataURL,
4777
5298
  exportMockup,
4778
5299
  exportPNG,
@@ -4785,6 +5306,7 @@ var AnnotationOverlay = class {
4785
5306
  isShapeMaskId,
4786
5307
  isTextureMaskId,
4787
5308
  needsAbsoluteSpace,
5309
+ normalizeTextCurve,
4788
5310
  readLayerShadow,
4789
5311
  renderTextureMask,
4790
5312
  resetTransform,
@@ -4793,6 +5315,8 @@ var AnnotationOverlay = class {
4793
5315
  sanitizeSvg,
4794
5316
  serializeEditor,
4795
5317
  shapeMaskPathData,
5318
+ textPathCurve,
5319
+ textPathSpec,
4796
5320
  toCanvasSpace,
4797
5321
  toHostSpace,
4798
5322
  unwrapGroup