@overtone-art/canvas-editor-core 0.3.2 → 0.4.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
@@ -45,17 +45,14 @@ __export(index_exports, {
45
45
  TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
46
46
  TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
47
47
  TextCurveManager: () => TextCurveManager,
48
+ TiledPatternObject: () => TiledPatternObject,
48
49
  UnitConverter: () => UnitConverter,
49
50
  applyAspectLock: () => applyAspectLock,
50
51
  applyLayerShadow: () => applyLayerShadow,
51
52
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
52
- applyPatternLocks: () => applyPatternLocks,
53
53
  applySelectionStyle: () => applySelectionStyle,
54
54
  buildCurvePathData: () => buildCurvePathData,
55
- buildPatternDataURL: () => buildPatternDataURL,
56
- captureLocks: () => captureLocks,
57
55
  clamp: () => clamp,
58
- clearPatternImageCache: () => clearPatternImageCache,
59
56
  clearTextureMaskCache: () => clearTextureMaskCache,
60
57
  computeCoverPlacement: () => computeCoverPlacement,
61
58
  computePrintAreaClip: () => computePrintAreaClip,
@@ -74,7 +71,6 @@ __export(index_exports, {
74
71
  isMaskPresetId: () => isMaskPresetId,
75
72
  isShapeMaskId: () => isShapeMaskId,
76
73
  isTextureMaskId: () => isTextureMaskId,
77
- loadPatternImage: () => loadPatternImage,
78
74
  readLayerShadow: () => readLayerShadow,
79
75
  renderTextureMask: () => renderTextureMask,
80
76
  resetTransform: () => resetTransform,
@@ -87,7 +83,7 @@ __export(index_exports, {
87
83
  module.exports = __toCommonJS(index_exports);
88
84
 
89
85
  // src/editor.ts
90
- var import_fabric9 = require("fabric");
86
+ var import_fabric10 = require("fabric");
91
87
 
92
88
  // src/events.ts
93
89
  var EventEmitter = class {
@@ -139,6 +135,12 @@ var Layer = class {
139
135
  /** Non-fabric data (e.g. pattern config) that must persist with the layer. */
140
136
  meta;
141
137
  fabricObject;
138
+ /**
139
+ * Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
140
+ * part of the canvas but never part of the document: it is not serialized, and
141
+ * the layer's real object stays the one every editor API talks to.
142
+ */
143
+ renderProxy;
142
144
  constructor(type, fabricObject, name, id) {
143
145
  this.id = id ?? generateId();
144
146
  this.type = type;
@@ -148,6 +150,7 @@ var Layer = class {
148
150
  this.opacity = 1;
149
151
  this.meta = {};
150
152
  this.fabricObject = fabricObject;
153
+ this.renderProxy = null;
151
154
  this.fabricObject._layerId = this.id;
152
155
  }
153
156
  hasMeta() {
@@ -196,6 +199,7 @@ var LayerManager = class {
196
199
  if (index === -1) return false;
197
200
  const layer = this.layers[index];
198
201
  this.canvas.remove(layer.fabricObject);
202
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
199
203
  this.layers.splice(index, 1);
200
204
  this.events.emit("layer:removed", { layerId: id });
201
205
  this.emitChanged();
@@ -219,6 +223,10 @@ var LayerManager = class {
219
223
  evented: !layer.locked
220
224
  });
221
225
  this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
226
+ if (layer.renderProxy) {
227
+ fabricObject.set({ opacity: 0 });
228
+ this.syncZOrder();
229
+ }
222
230
  if (wasActive) this.canvas.setActiveObject(fabricObject);
223
231
  this.canvas.requestRenderAll();
224
232
  this.events.emit("layer:modified", { layerId: id });
@@ -226,6 +234,36 @@ var LayerManager = class {
226
234
  this.onPropertyChanged?.();
227
235
  return true;
228
236
  }
237
+ /**
238
+ * Attach (or clear) the object drawn in place of a layer's own object. The
239
+ * proxy tracks the layer's stacking position, visibility and opacity, and is
240
+ * removed with the layer — it must never outlive or drift from its source.
241
+ */
242
+ setRenderProxy(id, proxy) {
243
+ const layer = this.get(id);
244
+ if (!layer || layer.renderProxy === proxy) return;
245
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
246
+ layer.renderProxy = proxy;
247
+ if (proxy) {
248
+ proxy.set({ visible: layer.visible, opacity: layer.opacity });
249
+ this.canvas.add(proxy);
250
+ this.syncZOrder();
251
+ }
252
+ this.canvas.requestRenderAll();
253
+ this.emitChanged();
254
+ }
255
+ /**
256
+ * Re-stack every canvas object to match layer order, keeping each proxy
257
+ * directly above the source it stands in for. Canvas indices can't be derived
258
+ * from layer indices once proxies are in the array, so the order is rebuilt
259
+ * front-to-back instead of computed.
260
+ */
261
+ syncZOrder() {
262
+ for (const layer of this.layers) {
263
+ this.canvas.bringObjectToFront(layer.fabricObject);
264
+ if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
265
+ }
266
+ }
229
267
  reorder(id, newIndex) {
230
268
  const oldIndex = this.layers.findIndex((l) => l.id === id);
231
269
  if (oldIndex === -1) return false;
@@ -234,9 +272,7 @@ var LayerManager = class {
234
272
  if (oldIndex === clamped) return false;
235
273
  const [layer] = this.layers.splice(oldIndex, 1);
236
274
  this.layers.splice(clamped, 0, layer);
237
- this.layers.forEach((l, i) => {
238
- this.canvas.moveObjectTo(l.fabricObject, i);
239
- });
275
+ this.syncZOrder();
240
276
  this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
241
277
  this.emitChanged();
242
278
  this.onPropertyChanged?.();
@@ -273,6 +309,7 @@ var LayerManager = class {
273
309
  if (layer.visible === visible) return;
274
310
  layer.visible = visible;
275
311
  layer.fabricObject.visible = visible;
312
+ if (layer.renderProxy) layer.renderProxy.visible = visible;
276
313
  this.canvas.requestRenderAll();
277
314
  this.emitChanged();
278
315
  this.onPropertyChanged?.();
@@ -294,7 +331,8 @@ var LayerManager = class {
294
331
  const next = Math.max(0, Math.min(1, opacity));
295
332
  if (layer.opacity === next) return;
296
333
  layer.opacity = next;
297
- layer.fabricObject.opacity = next;
334
+ if (layer.renderProxy) layer.renderProxy.opacity = next;
335
+ else layer.fabricObject.opacity = next;
298
336
  this.canvas.requestRenderAll();
299
337
  this.emitChanged();
300
338
  this.onPropertyChanged?.();
@@ -342,6 +380,7 @@ var LayerManager = class {
342
380
  clear() {
343
381
  for (const layer of this.layers) {
344
382
  this.canvas.remove(layer.fabricObject);
383
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
345
384
  }
346
385
  this.layers = [];
347
386
  this.emitChanged();
@@ -875,199 +914,415 @@ var CropController = class {
875
914
  };
876
915
  var STROKE2 = "#22c55e";
877
916
 
878
- // src/pattern.ts
917
+ // src/pattern/pattern-manager.ts
918
+ var import_fabric3 = require("fabric");
919
+
920
+ // src/pattern/tiled-pattern-object.ts
879
921
  var import_fabric2 = require("fabric");
922
+
923
+ // src/pattern/tile-geometry.ts
880
924
  var MAX_TILES_PER_AXIS = 200;
925
+ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
926
+ const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
927
+ const radius = cornerRadius(anchor, targetW, targetH);
928
+ const span = radius * 2;
929
+ const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
930
+ const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
931
+ const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
932
+ const cols = Math.ceil(span / tileW) + 2;
933
+ const rows = Math.ceil(span / tileH) + 2;
934
+ const halfCols = Math.ceil(cols / 2);
935
+ const halfRows = Math.ceil(rows / 2);
936
+ const shiftX = tileW * (clampOffset(config.offsetX) / 100);
937
+ const shiftY = tileH * (clampOffset(config.offsetY) / 100);
938
+ const placements = [];
939
+ for (let j = -halfRows; j <= halfRows; j++) {
940
+ for (let i = -halfCols; i <= halfCols; i++) {
941
+ let x = i * tileW + shiftX;
942
+ let y = j * tileH + shiftY;
943
+ if (config.mode === "brick-horizontal" && mod2(j) === 1) {
944
+ x += tileW * (config.horizontalOffset / 100);
945
+ } else if (config.mode === "brick-vertical" && mod2(i) === 1) {
946
+ y += tileH * (config.horizontalOffset / 100);
947
+ }
948
+ const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
949
+ placements.push({ x, y, rotation });
950
+ }
951
+ }
952
+ return placements;
953
+ }
954
+ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
955
+ const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
956
+ ctx.save();
957
+ ctx.translate(anchor.x, anchor.y);
958
+ ctx.rotate(config.angle * Math.PI / 180);
959
+ for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
960
+ ctx.save();
961
+ ctx.translate(tile.x, tile.y);
962
+ ctx.rotate(tile.rotation);
963
+ ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
964
+ ctx.restore();
965
+ }
966
+ ctx.restore();
967
+ }
968
+ function cornerRadius(origin, w, h) {
969
+ const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
970
+ const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
971
+ return Math.sqrt(dx * dx + dy * dy);
972
+ }
973
+ function clampOffset(value) {
974
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
975
+ return Math.max(-100, Math.min(100, value));
976
+ }
977
+ function mod2(n) {
978
+ return (n % 2 + 2) % 2;
979
+ }
980
+
981
+ // src/pattern/tiled-pattern-object.ts
982
+ var MAX_SNAPSHOT_PIXELS = 16e6;
983
+ var MAX_SNAPSHOT_SCALE = 8;
984
+ var SNAPSHOT_SHRINK_FACTOR = 2;
985
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
986
+ static type = "TiledPattern";
987
+ /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
988
+ source;
989
+ config;
990
+ snapshotEl = null;
991
+ snapshotScale = 0;
992
+ constructor(source, config, width, height) {
993
+ super({
994
+ // Explicit origin: fabric v7 defaults to `center`, and a centre-origin
995
+ // object placed at 0,0 covers a quarter of the canvas. That exact bug is
996
+ // why the old bake-into-the-layer pattern landed in the top-left corner.
997
+ originX: "left",
998
+ originY: "top",
999
+ left: 0,
1000
+ top: 0,
1001
+ width,
1002
+ height,
1003
+ // Pointer events belong to the source underneath, and fabric's object
1004
+ // cache would freeze the tiling at screen resolution.
1005
+ selectable: false,
1006
+ evented: false,
1007
+ objectCaching: false,
1008
+ hasControls: false,
1009
+ hasBorders: false
1010
+ });
1011
+ this.source = source;
1012
+ this.config = config;
1013
+ }
1014
+ /** Swap in a new config; the next render picks it up. */
1015
+ setConfig(config) {
1016
+ this.config = config;
1017
+ this.dirty = true;
1018
+ }
1019
+ /** Resize to a new print area. */
1020
+ setArea(width, height) {
1021
+ this.set({ width, height });
1022
+ this.setCoords();
1023
+ }
1024
+ /** Drop the cached source snapshot (e.g. the source was edited). */
1025
+ invalidate() {
1026
+ this.snapshotEl = null;
1027
+ this.snapshotScale = 0;
1028
+ this.dirty = true;
1029
+ }
1030
+ /** Free the offscreen snapshot. */
1031
+ dispose() {
1032
+ this.snapshotEl = null;
1033
+ this.snapshotScale = 0;
1034
+ }
1035
+ _render(ctx) {
1036
+ const width = this.width ?? 0;
1037
+ const height = this.height ?? 0;
1038
+ if (width <= 0 || height <= 0) return;
1039
+ const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
1040
+ const snapshot = this.ensureSnapshot(contextScale(ctx) * tileScale);
1041
+ if (!snapshot) return;
1042
+ const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
1043
+ ctx.save();
1044
+ ctx.translate(-width / 2, -height / 2);
1045
+ drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
1046
+ ctx.restore();
1047
+ }
1048
+ /** Raster fallback for SVG export — one `<image>` covering the print area. */
1049
+ _toSVG() {
1050
+ const width = this.width ?? 0;
1051
+ const height = this.height ?? 0;
1052
+ if (width <= 0 || height <= 0) return [];
1053
+ const el = document.createElement("canvas");
1054
+ el.width = Math.max(1, Math.round(width));
1055
+ el.height = Math.max(1, Math.round(height));
1056
+ const ctx = el.getContext("2d");
1057
+ if (!ctx) return [];
1058
+ const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
1059
+ const snapshot = this.ensureSnapshot(tileScale);
1060
+ if (!snapshot) return [];
1061
+ const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
1062
+ drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
1063
+ return [
1064
+ `<image x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" `,
1065
+ `xlink:href="${el.toDataURL("image/png")}"></image>
1066
+ `
1067
+ ];
1068
+ }
1069
+ /**
1070
+ * Fabric's generic clone round-trips through `toObject()` + the class
1071
+ * registry, which cannot carry a live source reference. Export paths clone
1072
+ * every canvas object, so without this a print export would silently lose the
1073
+ * tiling. The copy shares the source (it only ever reads from it).
1074
+ */
1075
+ clone() {
1076
+ const copy = new _TiledPatternObject(
1077
+ this.source,
1078
+ this.config,
1079
+ this.width ?? 0,
1080
+ this.height ?? 0
1081
+ );
1082
+ copy.set({
1083
+ left: this.left,
1084
+ top: this.top,
1085
+ visible: this.visible,
1086
+ opacity: this.opacity
1087
+ });
1088
+ return Promise.resolve(copy);
1089
+ }
1090
+ /**
1091
+ * The proxy holds a live reference to its source, which would make a
1092
+ * serialized canvas circular. Nothing persists this object (only layers are
1093
+ * serialized) — this keeps an accidental `canvas.toObject()` from throwing.
1094
+ */
1095
+ toObject() {
1096
+ const plain = super.toObject();
1097
+ delete plain.source;
1098
+ return plain;
1099
+ }
1100
+ /** Tile size in canvas units, floored so a tiny tile can't spawn a huge loop. */
1101
+ tileSize(snapshot, tileScale, width, height) {
1102
+ const floor = Math.max(2, Math.sqrt(width * width + height * height) / 200);
1103
+ return {
1104
+ tileW: Math.max(floor, snapshot.width / this.snapshotScale * tileScale),
1105
+ tileH: Math.max(floor, snapshot.height / this.snapshotScale * tileScale)
1106
+ };
1107
+ }
1108
+ /** Grid anchor: the source's centre, in this object's coordinates. */
1109
+ gridOrigin() {
1110
+ const centre = this.source.getCenterPoint();
1111
+ return { x: centre.x - (this.left ?? 0), y: centre.y - (this.top ?? 0) };
1112
+ }
1113
+ /**
1114
+ * Snapshot the source at (at least) `scale`, reusing the cached one when it is
1115
+ * still sharp enough and the source has not changed.
1116
+ */
1117
+ ensureSnapshot(scale) {
1118
+ const wanted = this.clampScale(scale);
1119
+ const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
1120
+ if (!stale) return this.snapshotEl;
1121
+ const source = this.source;
1122
+ const opacity = source.opacity;
1123
+ source.opacity = 1;
1124
+ try {
1125
+ const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
1126
+ if (!el.width || !el.height) return null;
1127
+ this.snapshotEl = el;
1128
+ this.snapshotScale = wanted;
1129
+ } catch {
1130
+ return this.snapshotEl;
1131
+ } finally {
1132
+ source.opacity = opacity;
1133
+ source.dirty = false;
1134
+ }
1135
+ return this.snapshotEl;
1136
+ }
1137
+ /** Bound the snapshot by both a linear scale and a total pixel budget. */
1138
+ clampScale(scale) {
1139
+ const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
1140
+ const rect = this.source.getBoundingRect();
1141
+ const area = Math.max(1, rect.width * rect.height);
1142
+ const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
1143
+ return Math.max(0.05, Math.min(requested, budgeted));
1144
+ }
1145
+ };
1146
+ function contextScale(ctx) {
1147
+ if (typeof ctx.getTransform !== "function") return 1;
1148
+ try {
1149
+ const t = ctx.getTransform();
1150
+ return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
1151
+ } catch {
1152
+ return 1;
1153
+ }
1154
+ }
1155
+
1156
+ // src/pattern/pattern-manager.ts
881
1157
  var PatternManager = class {
882
- constructor(canvas, layers, history, events, sourceResolver) {
1158
+ constructor(canvas, layers, history, events) {
883
1159
  this.canvas = canvas;
884
1160
  this.layers = layers;
885
1161
  this.history = history;
886
1162
  this.events = events;
887
- this.sourceResolver = sourceResolver;
1163
+ this.canvas.on("object:modified", this.onSourceModified);
1164
+ this.canvas.on("text:changed", this.onSourceModified);
1165
+ this.events.on("layer:removed", this.onLayerRemoved);
888
1166
  }
889
1167
  canvas;
890
1168
  layers;
891
1169
  history;
892
1170
  events;
893
- sourceResolver;
894
- // Per-layer task chain. apply()/disable() both await an async setSrc on the
895
- // same fabric image; running two concurrently lets their setSrc resolutions
896
- // interleave (wrong image installed, original lost). Serialising per layer
897
- // guarantees the last-requested operation wins and state stays consistent.
898
- chains = /* @__PURE__ */ new Map();
1171
+ proxies = /* @__PURE__ */ new Map();
1172
+ onSourceModified = (event) => {
1173
+ this.invalidateFor(event.target);
1174
+ };
1175
+ onLayerRemoved = ({ layerId }) => {
1176
+ const proxy = this.proxies.get(layerId);
1177
+ if (!proxy) return;
1178
+ proxy.dispose();
1179
+ this.proxies.delete(layerId);
1180
+ };
899
1181
  isPattern(layerId) {
900
1182
  return !!this.layers.get(layerId)?.meta.pattern;
901
1183
  }
902
1184
  getConfig(layerId) {
903
1185
  return this.layers.get(layerId)?.meta.pattern?.config ?? null;
904
1186
  }
905
- /** Turn a plain image layer into a pattern, or update an existing one. */
906
- apply(layerId, config) {
907
- return this.enqueue(layerId, async () => {
908
- const layer = this.layers.get(layerId);
909
- if (!layer || layer.type !== "image") return;
910
- const image = layer.fabricObject;
911
- const firstEnable = !layer.meta.pattern;
912
- if (!layer.meta.pattern) {
913
- const clip = image.clipPath;
914
- layer.meta.pattern = {
915
- config,
916
- originalSrc: elementToDataURL(image) ?? image.getSrc(),
917
- originalClip: clip ? clip.toObject() : null,
918
- originalLocks: captureLocks(image),
919
- original: {
920
- left: image.left ?? 0,
921
- top: image.top ?? 0,
922
- scaleX: image.scaleX ?? 1,
923
- scaleY: image.scaleY ?? 1,
924
- width: image.width ?? 0,
925
- height: image.height ?? 0,
926
- angle: image.angle ?? 0,
927
- cropX: image.cropX ?? 0,
928
- cropY: image.cropY ?? 0
929
- }
930
- };
1187
+ /** Turn a layer into a repeating pattern, or update an existing one. */
1188
+ async apply(layerId, config) {
1189
+ const layer = this.layers.get(layerId);
1190
+ if (!layer) return;
1191
+ try {
1192
+ this.layers.setMeta(layerId, { pattern: { config } });
1193
+ const proxy = this.proxies.get(layerId);
1194
+ if (proxy) {
1195
+ proxy.setConfig(config);
1196
+ this.canvas.requestRenderAll();
931
1197
  } else {
932
- layer.meta.pattern.config = config;
933
- }
934
- try {
935
- await this.renderLayer(layer);
936
- } catch (err) {
937
- if (firstEnable) delete layer.meta.pattern;
938
- throw err;
1198
+ this.attach(layer, config);
939
1199
  }
940
1200
  this.history.save();
941
- }).catch((error) => {
942
- this.events.emit("error", { message: "Failed to apply image pattern", error });
1201
+ } catch (error) {
1202
+ this.events.emit("error", { message: "Failed to apply pattern", error });
943
1203
  throw error;
944
- });
945
- }
946
- /**
947
- * Stretch every restored pattern layer back over the full print area and
948
- * re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
949
- * it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
950
- * after a state restore, where the canvas may be a different display size
951
- * than when the pattern was baked — and to repair states saved while a
952
- * pattern could still be dragged out of the print area.
953
- */
954
- repinAll() {
955
- const cw = this.canvas.getWidth();
956
- const ch = this.canvas.getHeight();
957
- let changed = false;
958
- for (const layer of this.layers.getAll()) {
959
- if (!layer.meta.pattern || layer.type !== "image") continue;
960
- const image = layer.fabricObject;
961
- image.set({
962
- left: 0,
963
- top: 0,
964
- angle: 0,
965
- scaleX: cw / (image.width || cw),
966
- scaleY: ch / (image.height || ch)
967
- });
968
- applyPatternLocks(image);
969
- image.setCoords();
970
- changed = true;
971
1204
  }
972
- if (changed) this.canvas.requestRenderAll();
973
1205
  }
974
- /** Restore the original image and drop the pattern. */
975
- disable(layerId) {
976
- return this.enqueue(layerId, async () => {
977
- const layer = this.layers.get(layerId);
978
- const state = layer?.meta.pattern;
979
- if (!layer || !state) return;
980
- const image = layer.fabricObject;
981
- await image.setSrc(state.originalSrc);
982
- image.set({
983
- left: state.original.left,
984
- top: state.original.top,
985
- scaleX: state.original.scaleX,
986
- scaleY: state.original.scaleY,
987
- width: state.original.width,
988
- height: state.original.height,
989
- cropX: state.original.cropX,
990
- cropY: state.original.cropY,
991
- angle: state.original.angle
992
- });
993
- image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
994
- restoreLocks(image, state.originalLocks);
995
- image.setCoords();
996
- delete layer.meta.pattern;
1206
+ /** Drop the tiling and show the source again. */
1207
+ async disable(layerId) {
1208
+ const layer = this.layers.get(layerId);
1209
+ if (!layer?.meta.pattern) return;
1210
+ try {
1211
+ this.detach(layerId);
1212
+ restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
1213
+ layer.fabricObject.set({ opacity: layer.opacity });
1214
+ this.layers.setMeta(layerId, { pattern: void 0 });
997
1215
  this.canvas.requestRenderAll();
998
1216
  this.history.save();
999
- }).catch((error) => {
1000
- this.events.emit("error", { message: "Failed to clear image pattern", error });
1217
+ } catch (error) {
1218
+ this.events.emit("error", { message: "Failed to clear pattern", error });
1001
1219
  throw error;
1002
- });
1220
+ }
1003
1221
  }
1004
- /** Run `task` after any in-flight work for this layer, regardless of outcome. */
1005
- enqueue(layerId, task) {
1006
- const prev = this.chains.get(layerId) ?? Promise.resolve();
1007
- const next = prev.then(task, task);
1008
- this.chains.set(
1009
- layerId,
1010
- next.catch(() => void 0)
1011
- );
1012
- return next;
1222
+ /**
1223
+ * Rebuild every proxy after a state restore, migrating any layer that was
1224
+ * saved by the old bake-into-the-layer engine.
1225
+ */
1226
+ async rehydrateAll() {
1227
+ this.clearProxies();
1228
+ for (const layer of this.layers.getAll()) {
1229
+ const state = layer.meta.pattern;
1230
+ if (!state) continue;
1231
+ try {
1232
+ if (isLegacyState(state)) {
1233
+ await unbakeLegacyLayer(layer, state);
1234
+ this.layers.setMeta(layer.id, { pattern: { config: state.config } });
1235
+ }
1236
+ this.attach(layer, state.config);
1237
+ } catch (error) {
1238
+ this.events.emit("error", { message: "Failed to restore pattern layer", error });
1239
+ }
1240
+ }
1241
+ this.canvas.requestRenderAll();
1013
1242
  }
1014
- async renderLayer(layer) {
1243
+ /** Re-fit every proxy to the print area (canvas resize). */
1244
+ syncArea() {
1245
+ const width = this.canvas.getWidth();
1246
+ const height = this.canvas.getHeight();
1247
+ for (const proxy of this.proxies.values()) {
1248
+ proxy.setArea(width, height);
1249
+ proxy.invalidate();
1250
+ }
1251
+ if (this.proxies.size > 0) this.canvas.requestRenderAll();
1252
+ }
1253
+ /** Drop a layer's cached source snapshot (its content changed). */
1254
+ invalidate(layerId) {
1255
+ const proxy = this.proxies.get(layerId);
1256
+ if (!proxy) return;
1257
+ proxy.invalidate();
1258
+ this.canvas.requestRenderAll();
1259
+ }
1260
+ /** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
1261
+ attachTo(layer) {
1015
1262
  const state = layer.meta.pattern;
1016
- if (!state) return;
1017
- const image = layer.fabricObject;
1018
- const cw = this.canvas.getWidth();
1019
- const ch = this.canvas.getHeight();
1020
- const scale = Math.max(1, state.config.scale ?? 100) / 100;
1021
- const diag = Math.sqrt(cw * cw + ch * ch);
1022
- const minTile = Math.max(2, diag / MAX_TILES_PER_AXIS);
1023
- const tileW = Math.max(minTile, state.original.width * state.original.scaleX * scale);
1024
- const tileH = Math.max(minTile, state.original.height * state.original.scaleY * scale);
1025
- const dataUrl = await buildPatternDataURL(
1026
- state.originalSrc,
1027
- state.config,
1028
- cw,
1029
- ch,
1030
- tileW,
1031
- tileH,
1032
- this.sourceResolver
1263
+ if (!state || this.proxies.has(layer.id)) return;
1264
+ this.attach(layer, state.config);
1265
+ }
1266
+ dispose() {
1267
+ this.canvas.off("object:modified", this.onSourceModified);
1268
+ this.canvas.off("text:changed", this.onSourceModified);
1269
+ this.events.off("layer:removed", this.onLayerRemoved);
1270
+ this.clearProxies();
1271
+ }
1272
+ /** Release every proxy and the offscreen snapshot it holds. */
1273
+ clearProxies() {
1274
+ for (const proxy of this.proxies.values()) proxy.dispose();
1275
+ this.proxies.clear();
1276
+ }
1277
+ attach(layer, config) {
1278
+ const proxy = new TiledPatternObject(
1279
+ layer.fabricObject,
1280
+ config,
1281
+ this.canvas.getWidth(),
1282
+ this.canvas.getHeight()
1033
1283
  );
1034
- await image.setSrc(dataUrl);
1035
- image.set({
1036
- left: 0,
1037
- top: 0,
1038
- scaleX: 1,
1039
- scaleY: 1,
1040
- width: cw,
1041
- height: ch,
1042
- cropX: 0,
1043
- cropY: 0,
1044
- angle: 0
1045
- });
1046
- image.clipPath = void 0;
1047
- applyPatternLocks(image);
1048
- image.setCoords();
1284
+ layer.fabricObject.set({ opacity: 0 });
1285
+ this.proxies.set(layer.id, proxy);
1286
+ this.layers.setRenderProxy(layer.id, proxy);
1049
1287
  this.canvas.requestRenderAll();
1050
1288
  }
1289
+ detach(layerId) {
1290
+ const proxy = this.proxies.get(layerId);
1291
+ if (!proxy) {
1292
+ this.layers.setRenderProxy(layerId, null);
1293
+ return;
1294
+ }
1295
+ this.layers.setRenderProxy(layerId, null);
1296
+ proxy.dispose();
1297
+ this.proxies.delete(layerId);
1298
+ }
1299
+ invalidateFor(target) {
1300
+ if (!target) return;
1301
+ const layer = this.layers.findByObject(target);
1302
+ if (layer) this.invalidate(layer.id);
1303
+ }
1051
1304
  };
1052
- function captureLocks(obj) {
1053
- return {
1054
- lockMovementX: obj.lockMovementX ?? false,
1055
- lockMovementY: obj.lockMovementY ?? false,
1056
- lockScalingX: obj.lockScalingX ?? false,
1057
- lockScalingY: obj.lockScalingY ?? false,
1058
- lockRotation: obj.lockRotation ?? false,
1059
- hasControls: obj.hasControls ?? true
1060
- };
1061
- }
1062
- function applyPatternLocks(obj) {
1063
- obj.set({
1064
- lockMovementX: true,
1065
- lockMovementY: true,
1066
- lockScalingX: true,
1067
- lockScalingY: true,
1068
- lockRotation: true,
1069
- hasControls: false
1305
+ function isLegacyState(state) {
1306
+ return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
1307
+ }
1308
+ async function unbakeLegacyLayer(layer, state) {
1309
+ const image = layer.fabricObject;
1310
+ if (typeof image.setSrc !== "function" || !state.original) return;
1311
+ await image.setSrc(state.originalSrc);
1312
+ image.set({
1313
+ left: state.original.left,
1314
+ top: state.original.top,
1315
+ scaleX: state.original.scaleX,
1316
+ scaleY: state.original.scaleY,
1317
+ width: state.original.width,
1318
+ height: state.original.height,
1319
+ cropX: state.original.cropX,
1320
+ cropY: state.original.cropY,
1321
+ angle: state.original.angle
1070
1322
  });
1323
+ image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
1324
+ restoreLocks(image, state.originalLocks);
1325
+ image.setCoords();
1071
1326
  }
1072
1327
  function restoreLocks(obj, locks) {
1073
1328
  obj.set(
@@ -1081,124 +1336,9 @@ function restoreLocks(obj, locks) {
1081
1336
  }
1082
1337
  );
1083
1338
  }
1084
- function elementToDataURL(image) {
1085
- try {
1086
- const el = image.getElement();
1087
- const w = el.naturalWidth || el.width;
1088
- const h = el.naturalHeight || el.height;
1089
- if (!w || !h) return null;
1090
- const off = document.createElement("canvas");
1091
- off.width = w;
1092
- off.height = h;
1093
- const ctx = off.getContext("2d");
1094
- if (!ctx) return null;
1095
- ctx.drawImage(el, 0, 0);
1096
- return off.toDataURL("image/png");
1097
- } catch {
1098
- return null;
1099
- }
1100
- }
1101
- var IMAGE_CACHE_MAX = 16;
1102
- var imageCache = /* @__PURE__ */ new Map();
1103
- function loadPatternImage(src, resolver) {
1104
- const cached = imageCache.get(src);
1105
- if (cached) {
1106
- imageCache.delete(src);
1107
- imageCache.set(src, cached);
1108
- return cached;
1109
- }
1110
- const promise = decodeImage(src).catch(async (originalError) => {
1111
- if (!resolver) throw originalError;
1112
- const resolved = await resolver(src);
1113
- if (!resolved || resolved === src) {
1114
- throw new Error("Pattern source resolver did not return a usable alternate URL", {
1115
- cause: originalError
1116
- });
1117
- }
1118
- return decodeImage(resolved);
1119
- });
1120
- promise.catch(() => {
1121
- if (imageCache.get(src) === promise) imageCache.delete(src);
1122
- });
1123
- imageCache.set(src, promise);
1124
- if (imageCache.size > IMAGE_CACHE_MAX) {
1125
- const oldest = imageCache.keys().next().value;
1126
- if (oldest !== void 0) imageCache.delete(oldest);
1127
- }
1128
- return promise;
1129
- }
1130
- function decodeImage(src) {
1131
- return new Promise((resolve, reject) => {
1132
- const img = new Image();
1133
- img.crossOrigin = "anonymous";
1134
- img.onload = () => resolve(img);
1135
- img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
1136
- img.src = src;
1137
- });
1138
- }
1139
- function clearPatternImageCache() {
1140
- imageCache.clear();
1141
- }
1142
- async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
1143
- const img = await loadPatternImage(src, sourceResolver);
1144
- const off = document.createElement("canvas");
1145
- off.width = Math.max(1, Math.round(targetW));
1146
- off.height = Math.max(1, Math.round(targetH));
1147
- const ctx = off.getContext("2d");
1148
- if (!ctx) return off.toDataURL("image/png");
1149
- drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
1150
- return off.toDataURL("image/png");
1151
- }
1152
- function computeTilePositions(config, targetW, targetH, baseW, baseH) {
1153
- const diag = Math.sqrt(targetW * targetW + targetH * targetH);
1154
- const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
1155
- const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
1156
- const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
1157
- const cols = Math.ceil(diag / tileW) + 2;
1158
- const rows = Math.ceil(diag / tileH) + 2;
1159
- const halfCols = Math.ceil(cols / 2);
1160
- const halfRows = Math.ceil(rows / 2);
1161
- const shiftX = tileW * (clampOffset(config.offsetX) / 100);
1162
- const shiftY = tileH * (clampOffset(config.offsetY) / 100);
1163
- const placements = [];
1164
- for (let j = -halfRows; j <= halfRows; j++) {
1165
- for (let i = -halfCols; i <= halfCols; i++) {
1166
- let x = i * tileW + shiftX;
1167
- let y = j * tileH + shiftY;
1168
- if (config.mode === "brick-horizontal" && mod2(j) === 1) {
1169
- x += tileW * (config.horizontalOffset / 100);
1170
- } else if (config.mode === "brick-vertical" && mod2(i) === 1) {
1171
- y += tileH * (config.horizontalOffset / 100);
1172
- }
1173
- const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
1174
- placements.push({ x, y, rotation });
1175
- }
1176
- }
1177
- return placements;
1178
- }
1179
- function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
1180
- ctx.save();
1181
- ctx.translate(targetW / 2, targetH / 2);
1182
- ctx.rotate(config.angle * Math.PI / 180);
1183
- for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
1184
- ctx.save();
1185
- ctx.translate(tile.x, tile.y);
1186
- ctx.rotate(tile.rotation);
1187
- ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
1188
- ctx.restore();
1189
- }
1190
- ctx.restore();
1191
- }
1192
- function clampOffset(value) {
1193
- if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1194
- return Math.max(-100, Math.min(100, value));
1195
- }
1196
- function mod2(n) {
1197
- return (n % 2 + 2) % 2;
1198
- }
1199
1339
 
1200
1340
  // src/text-curve.ts
1201
- var import_fabric3 = require("fabric");
1341
+ var import_fabric4 = require("fabric");
1202
1342
  var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
1203
1343
  var MIN_ARC = 0.5;
1204
1344
  var FULL_CIRCLE_ARC = 99.5;
@@ -1321,7 +1461,7 @@ var TextCurveManager = class {
1321
1461
  if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
1322
1462
  text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
1323
1463
  text.set({
1324
- path: new import_fabric3.Path(curve.data, { visible: false, objectCaching: false }),
1464
+ path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
1325
1465
  pathAlign: "center",
1326
1466
  pathSide: "left",
1327
1467
  pathStartOffset: Math.max(0, (curve.length - run) / 2)
@@ -1363,7 +1503,7 @@ var TextCurveManager = class {
1363
1503
  };
1364
1504
 
1365
1505
  // src/mask-presets/manager.ts
1366
- var import_fabric4 = require("fabric");
1506
+ var import_fabric5 = require("fabric");
1367
1507
 
1368
1508
  // src/mask-presets/shapes.ts
1369
1509
  var SHAPE_MASK_IDS = [
@@ -1630,13 +1770,13 @@ var MaskPresetManager = class {
1630
1770
  objectCaching: false
1631
1771
  };
1632
1772
  if (isShapeMaskId(id)) {
1633
- return new import_fabric4.Path(shapeMaskPathData(id), {
1773
+ return new import_fabric5.Path(shapeMaskPathData(id), {
1634
1774
  ...shared,
1635
1775
  scaleX: width / SHAPE_MASK_BOX,
1636
1776
  scaleY: height / SHAPE_MASK_BOX
1637
1777
  });
1638
1778
  }
1639
- return new import_fabric4.FabricImage(renderTextureMask(id), {
1779
+ return new import_fabric5.FabricImage(renderTextureMask(id), {
1640
1780
  ...shared,
1641
1781
  scaleX: width / TEXTURE_MASK_SIZE,
1642
1782
  scaleY: height / TEXTURE_MASK_SIZE
@@ -1645,7 +1785,7 @@ var MaskPresetManager = class {
1645
1785
  };
1646
1786
 
1647
1787
  // src/shadow.ts
1648
- var import_fabric5 = require("fabric");
1788
+ var import_fabric6 = require("fabric");
1649
1789
 
1650
1790
  // src/utils/color.ts
1651
1791
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -1683,7 +1823,7 @@ function applyLayerShadow(object, config) {
1683
1823
  }
1684
1824
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
1685
1825
  object.set({
1686
- shadow: new import_fabric5.Shadow({
1826
+ shadow: new import_fabric6.Shadow({
1687
1827
  color,
1688
1828
  blur: Math.max(0, next.blur),
1689
1829
  offsetX: next.offsetX,
@@ -1797,7 +1937,7 @@ var UnitConverter = class {
1797
1937
  };
1798
1938
 
1799
1939
  // src/serialization.ts
1800
- var import_fabric6 = require("fabric");
1940
+ var import_fabric7 = require("fabric");
1801
1941
  var VERSION = "2.0.0";
1802
1942
  function serializeEditor(editor) {
1803
1943
  return {
@@ -1833,7 +1973,7 @@ async function deserializeEditor(editor, state) {
1833
1973
  }
1834
1974
  const staged = await Promise.all(
1835
1975
  state.layers.map(async (serialized) => {
1836
- const fabricObject = (await import_fabric6.util.enlivenObjects([serialized.fabricObject]))[0];
1976
+ const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
1837
1977
  if (!fabricObject) {
1838
1978
  const source = serialized.fabricObject.src;
1839
1979
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -1844,7 +1984,7 @@ async function deserializeEditor(editor, state) {
1844
1984
  return { serialized, fabricObject };
1845
1985
  })
1846
1986
  );
1847
- const stagedBackground = state.backgroundImage ? (await import_fabric6.util.enlivenObjects([state.backgroundImage]))[0] : null;
1987
+ const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
1848
1988
  if (state.backgroundImage && !stagedBackground) {
1849
1989
  const source = state.backgroundImage.src;
1850
1990
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -1891,7 +2031,7 @@ function restoreLayer(editor, serialized, fabricObject) {
1891
2031
  }
1892
2032
 
1893
2033
  // src/export.ts
1894
- var import_fabric7 = require("fabric");
2034
+ var import_fabric8 = require("fabric");
1895
2035
 
1896
2036
  // src/displacement.ts
1897
2037
  var CHANNEL_INDEX = {
@@ -1985,7 +2125,7 @@ async function exportPNG(canvas, options = {}) {
1985
2125
  }
1986
2126
  async function exportIsolatedPNG(source, objects, options = {}) {
1987
2127
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
1988
- const canvas = new import_fabric7.StaticCanvas(element, {
2128
+ const canvas = new import_fabric8.StaticCanvas(element, {
1989
2129
  width: options.width ?? source.getWidth(),
1990
2130
  height: options.height ?? source.getHeight(),
1991
2131
  backgroundColor: options.backgroundColor || void 0
@@ -2015,7 +2155,7 @@ async function exportPrintArea(source, area, options = {}) {
2015
2155
  throw new Error("Print area does not overlap the canvas");
2016
2156
  }
2017
2157
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2018
- const canvas = new import_fabric7.StaticCanvas(element, { width, height });
2158
+ const canvas = new import_fabric8.StaticCanvas(element, { width, height });
2019
2159
  try {
2020
2160
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2021
2161
  if (clones.length) canvas.add(...clones);
@@ -2455,7 +2595,7 @@ var ProjectManager = class {
2455
2595
  };
2456
2596
 
2457
2597
  // src/mask.ts
2458
- var import_fabric8 = require("fabric");
2598
+ var import_fabric9 = require("fabric");
2459
2599
  var MaskRefinementError = class extends Error {
2460
2600
  constructor(code, message, cause) {
2461
2601
  super(message);
@@ -2487,7 +2627,7 @@ var MaskController = class {
2487
2627
  throw new Error("Mask dimensions must be positive integers");
2488
2628
  }
2489
2629
  const backing = this.makeCanvas(width, height);
2490
- const image = new import_fabric8.FabricImage(backing, {
2630
+ const image = new import_fabric9.FabricImage(backing, {
2491
2631
  left: 0,
2492
2632
  top: 0,
2493
2633
  originX: "left",
@@ -2753,7 +2893,7 @@ var CanvasEditor = class {
2753
2893
  const widthPx = this.units.toPixels(config.width);
2754
2894
  const heightPx = this.units.toPixels(config.height);
2755
2895
  this.designBackground = config.backgroundColor ?? "#ffffff";
2756
- this.canvas = new import_fabric9.Canvas(canvasElement, {
2896
+ this.canvas = new import_fabric10.Canvas(canvasElement, {
2757
2897
  width: widthPx,
2758
2898
  height: heightPx,
2759
2899
  backgroundColor: this.designBackground,
@@ -2774,13 +2914,7 @@ var CanvasEditor = class {
2774
2914
  this.layers.setHistoryCallback(() => this.history.save());
2775
2915
  this.snapping = new SnapManager(this.canvas, this.events);
2776
2916
  this.crop = new CropController(this.canvas, this.history, this.events);
2777
- this.patterns = new PatternManager(
2778
- this.canvas,
2779
- this.layers,
2780
- this.history,
2781
- this.events,
2782
- config.patternSourceResolver
2783
- );
2917
+ this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
2784
2918
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
2785
2919
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
2786
2920
  this.setupCanvasEvents();
@@ -2792,7 +2926,7 @@ var CanvasEditor = class {
2792
2926
  // ─── Layer Operations ────────────────────────────────
2793
2927
  async addImage(url, options) {
2794
2928
  try {
2795
- const img = await import_fabric9.FabricImage.fromURL(
2929
+ const img = await import_fabric10.FabricImage.fromURL(
2796
2930
  url,
2797
2931
  {},
2798
2932
  { originX: "left", originY: "top", ...options }
@@ -2817,7 +2951,7 @@ var CanvasEditor = class {
2817
2951
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
2818
2952
  const previous = layer.fabricObject;
2819
2953
  try {
2820
- const replacement = await import_fabric9.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2954
+ const replacement = await import_fabric10.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2821
2955
  replacement.set({
2822
2956
  left: previous.left,
2823
2957
  top: previous.top,
@@ -2846,7 +2980,7 @@ var CanvasEditor = class {
2846
2980
  }
2847
2981
  }
2848
2982
  addText(text, options) {
2849
- const textbox = new import_fabric9.Textbox(text, {
2983
+ const textbox = new import_fabric10.Textbox(text, {
2850
2984
  fontSize: 32,
2851
2985
  fontFamily: "Arial",
2852
2986
  fill: "#000000",
@@ -2885,10 +3019,10 @@ var CanvasEditor = class {
2885
3019
  return value === void 0 ? token : escapeXml(value);
2886
3020
  })
2887
3021
  );
2888
- const { objects, options } = await (0, import_fabric9.loadSVGFromString)(resolved);
3022
+ const { objects, options } = await (0, import_fabric10.loadSVGFromString)(resolved);
2889
3023
  const validObjects = objects.filter((object) => object !== null);
2890
3024
  if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
2891
- const group = import_fabric9.util.groupSVGElements(validObjects, options);
3025
+ const group = import_fabric10.util.groupSVGElements(validObjects, options);
2892
3026
  group.set({
2893
3027
  left: this.canvas.getWidth() / 2,
2894
3028
  top: this.canvas.getHeight() / 2,
@@ -2933,9 +3067,7 @@ var CanvasEditor = class {
2933
3067
  const layer = this.layers.get(id);
2934
3068
  if (!layer) return null;
2935
3069
  const clone = await layer.fabricObject.clone();
2936
- if (!layer.meta.pattern) {
2937
- clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
2938
- }
3070
+ clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
2939
3071
  clone.setCoords();
2940
3072
  const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
2941
3073
  copy.meta = structuredClone(layer.meta);
@@ -2948,6 +3080,7 @@ var CanvasEditor = class {
2948
3080
  evented: !layer.locked,
2949
3081
  opacity: layer.opacity
2950
3082
  });
3083
+ this.patterns.attachTo(copy);
2951
3084
  this.canvas.setActiveObject(clone);
2952
3085
  this.canvas.requestRenderAll();
2953
3086
  this.history.save();
@@ -2961,10 +3094,10 @@ var CanvasEditor = class {
2961
3094
  const next = { ...previous, ...adjustments };
2962
3095
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
2963
3096
  image.filters = [
2964
- new import_fabric9.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
2965
- new import_fabric9.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
2966
- new import_fabric9.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
2967
- new import_fabric9.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
3097
+ new import_fabric10.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
3098
+ new import_fabric10.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
3099
+ new import_fabric10.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
3100
+ new import_fabric10.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
2968
3101
  ];
2969
3102
  layer.meta.imageAdjustments = next;
2970
3103
  image.applyFilters();
@@ -2981,7 +3114,7 @@ var CanvasEditor = class {
2981
3114
  const childData = children.map((layer) => structuredClone(layer.toData()));
2982
3115
  const objects = children.map((layer) => layer.fabricObject);
2983
3116
  for (const layer of children) this.layers.remove(layer.id);
2984
- const group = new import_fabric9.Group(objects);
3117
+ const group = new import_fabric10.Group(objects);
2985
3118
  const grouped = this.layers.add("group", group, name);
2986
3119
  grouped.meta.groupChildren = childData;
2987
3120
  this.layers.select(grouped.id);
@@ -3000,7 +3133,7 @@ var CanvasEditor = class {
3000
3133
  const objects = group.removeAll();
3001
3134
  this.layers.remove(id);
3002
3135
  const restored = objects.map((object, index) => {
3003
- import_fabric9.util.addTransformToObject(object, transform);
3136
+ import_fabric10.util.addTransformToObject(object, transform);
3004
3137
  object.setCoords();
3005
3138
  const data = childData[index];
3006
3139
  const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
@@ -3027,7 +3160,7 @@ var CanvasEditor = class {
3027
3160
  }
3028
3161
  try {
3029
3162
  await deserializeEditor(this, state);
3030
- this.patterns.repinAll();
3163
+ await this.patterns.rehydrateAll();
3031
3164
  } catch (error) {
3032
3165
  if (!managedByHistory) {
3033
3166
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -3056,7 +3189,7 @@ var CanvasEditor = class {
3056
3189
  const layer = this.layers.get(id);
3057
3190
  if (!layer) throw new Error(`Layer not found: ${id}`);
3058
3191
  try {
3059
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric9.FabricImage) {
3192
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric10.FabricImage) {
3060
3193
  const image = await layer.fabricObject.clone();
3061
3194
  image.set({
3062
3195
  left: 0,
@@ -3076,7 +3209,11 @@ var CanvasEditor = class {
3076
3209
  cloneObjects: false
3077
3210
  });
3078
3211
  }
3079
- return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
3212
+ return await exportIsolatedPNG(
3213
+ this.canvas,
3214
+ [layer.renderProxy ?? layer.fabricObject],
3215
+ options
3216
+ );
3080
3217
  } catch (error) {
3081
3218
  this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
3082
3219
  throw error;
@@ -3328,7 +3465,7 @@ var CanvasEditor = class {
3328
3465
  return;
3329
3466
  }
3330
3467
  try {
3331
- const image = await import_fabric9.FabricImage.fromURL(
3468
+ const image = await import_fabric10.FabricImage.fromURL(
3332
3469
  url,
3333
3470
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
3334
3471
  { originX: "left", originY: "top" }
@@ -3381,7 +3518,6 @@ var CanvasEditor = class {
3381
3518
  const sx = widthPx / oldWidth;
3382
3519
  const sy = heightPx / oldHeight;
3383
3520
  for (const layer of this.layers.getAll()) {
3384
- if (layer.meta.pattern) continue;
3385
3521
  const object = layer.fabricObject;
3386
3522
  object.set({
3387
3523
  left: (object.left ?? 0) * sx,
@@ -3402,7 +3538,7 @@ var CanvasEditor = class {
3402
3538
  }
3403
3539
  }
3404
3540
  this.canvas.setDimensions({ width: widthPx, height: heightPx });
3405
- this.patterns.repinAll();
3541
+ this.patterns.syncArea();
3406
3542
  this.canvas.requestRenderAll();
3407
3543
  this.history.save();
3408
3544
  this.events.emit("canvas:modified", {});
@@ -3600,7 +3736,7 @@ var CanvasEditor = class {
3600
3736
  this.snapping.dispose();
3601
3737
  this.crop.dispose();
3602
3738
  this.history.dispose();
3603
- clearPatternImageCache();
3739
+ this.patterns.dispose();
3604
3740
  this.events.removeAllListeners();
3605
3741
  this.canvas.dispose();
3606
3742
  }
@@ -3744,17 +3880,14 @@ var AnnotationOverlay = class {
3744
3880
  TEXTURE_MASK_IDS,
3745
3881
  TEXTURE_MASK_SIZE,
3746
3882
  TextCurveManager,
3883
+ TiledPatternObject,
3747
3884
  UnitConverter,
3748
3885
  applyAspectLock,
3749
3886
  applyLayerShadow,
3750
3887
  applyObjectSelectionStyle,
3751
- applyPatternLocks,
3752
3888
  applySelectionStyle,
3753
3889
  buildCurvePathData,
3754
- buildPatternDataURL,
3755
- captureLocks,
3756
3890
  clamp,
3757
- clearPatternImageCache,
3758
3891
  clearTextureMaskCache,
3759
3892
  computeCoverPlacement,
3760
3893
  computePrintAreaClip,
@@ -3773,7 +3906,6 @@ var AnnotationOverlay = class {
3773
3906
  isMaskPresetId,
3774
3907
  isShapeMaskId,
3775
3908
  isTextureMaskId,
3776
- loadPatternImage,
3777
3909
  readLayerShadow,
3778
3910
  renderTextureMask,
3779
3911
  resetTransform,