@overtone-art/canvas-editor-core 0.3.2 → 0.5.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
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AnnotationOverlay: () => AnnotationOverlay,
24
+ CANVAS_MASK_TARGET: () => CANVAS_MASK_TARGET,
24
25
  CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
25
26
  CanvasEditor: () => CanvasEditor,
26
27
  CropController: () => CropController,
@@ -33,6 +34,7 @@ __export(index_exports, {
33
34
  HistoryManager: () => HistoryManager,
34
35
  Layer: () => Layer,
35
36
  LayerManager: () => LayerManager,
37
+ LayerMaskManager: () => LayerMaskManager,
36
38
  LicenseManager: () => LicenseManager,
37
39
  MaskController: () => MaskController,
38
40
  MaskPresetManager: () => MaskPresetManager,
@@ -45,18 +47,16 @@ __export(index_exports, {
45
47
  TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
46
48
  TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
47
49
  TextCurveManager: () => TextCurveManager,
50
+ TiledPatternObject: () => TiledPatternObject,
48
51
  UnitConverter: () => UnitConverter,
49
52
  applyAspectLock: () => applyAspectLock,
50
53
  applyLayerShadow: () => applyLayerShadow,
51
54
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
52
- applyPatternLocks: () => applyPatternLocks,
53
55
  applySelectionStyle: () => applySelectionStyle,
54
56
  buildCurvePathData: () => buildCurvePathData,
55
- buildPatternDataURL: () => buildPatternDataURL,
56
- captureLocks: () => captureLocks,
57
57
  clamp: () => clamp,
58
- clearPatternImageCache: () => clearPatternImageCache,
59
58
  clearTextureMaskCache: () => clearTextureMaskCache,
59
+ composeMaskGroup: () => composeMaskGroup,
60
60
  computeCoverPlacement: () => computeCoverPlacement,
61
61
  computePrintAreaClip: () => computePrintAreaClip,
62
62
  computeTilePositions: () => computeTilePositions,
@@ -69,12 +69,13 @@ __export(index_exports, {
69
69
  exportPNG: () => exportPNG,
70
70
  exportPrintArea: () => exportPrintArea,
71
71
  exportSVG: () => exportSVG,
72
+ fitToBox: () => fitToBox,
72
73
  generateId: () => generateId,
73
74
  isCssColor: () => isCssColor,
74
75
  isMaskPresetId: () => isMaskPresetId,
75
76
  isShapeMaskId: () => isShapeMaskId,
76
77
  isTextureMaskId: () => isTextureMaskId,
77
- loadPatternImage: () => loadPatternImage,
78
+ needsAbsoluteSpace: () => needsAbsoluteSpace,
78
79
  readLayerShadow: () => readLayerShadow,
79
80
  renderTextureMask: () => renderTextureMask,
80
81
  resetTransform: () => resetTransform,
@@ -82,12 +83,15 @@ __export(index_exports, {
82
83
  round2: () => round2,
83
84
  sanitizeSvg: () => sanitizeSvg,
84
85
  serializeEditor: () => serializeEditor,
85
- shapeMaskPathData: () => shapeMaskPathData
86
+ shapeMaskPathData: () => shapeMaskPathData,
87
+ toCanvasSpace: () => toCanvasSpace,
88
+ toHostSpace: () => toHostSpace,
89
+ unwrapGroup: () => unwrapGroup
86
90
  });
87
91
  module.exports = __toCommonJS(index_exports);
88
92
 
89
93
  // src/editor.ts
90
- var import_fabric9 = require("fabric");
94
+ var import_fabric17 = require("fabric");
91
95
 
92
96
  // src/events.ts
93
97
  var EventEmitter = class {
@@ -139,6 +143,12 @@ var Layer = class {
139
143
  /** Non-fabric data (e.g. pattern config) that must persist with the layer. */
140
144
  meta;
141
145
  fabricObject;
146
+ /**
147
+ * Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
148
+ * part of the canvas but never part of the document: it is not serialized, and
149
+ * the layer's real object stays the one every editor API talks to.
150
+ */
151
+ renderProxy;
142
152
  constructor(type, fabricObject, name, id) {
143
153
  this.id = id ?? generateId();
144
154
  this.type = type;
@@ -148,6 +158,7 @@ var Layer = class {
148
158
  this.opacity = 1;
149
159
  this.meta = {};
150
160
  this.fabricObject = fabricObject;
161
+ this.renderProxy = null;
151
162
  this.fabricObject._layerId = this.id;
152
163
  }
153
164
  hasMeta() {
@@ -196,6 +207,7 @@ var LayerManager = class {
196
207
  if (index === -1) return false;
197
208
  const layer = this.layers[index];
198
209
  this.canvas.remove(layer.fabricObject);
210
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
199
211
  this.layers.splice(index, 1);
200
212
  this.events.emit("layer:removed", { layerId: id });
201
213
  this.emitChanged();
@@ -219,6 +231,10 @@ var LayerManager = class {
219
231
  evented: !layer.locked
220
232
  });
221
233
  this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
234
+ if (layer.renderProxy) {
235
+ fabricObject.set({ opacity: 0 });
236
+ this.syncZOrder();
237
+ }
222
238
  if (wasActive) this.canvas.setActiveObject(fabricObject);
223
239
  this.canvas.requestRenderAll();
224
240
  this.events.emit("layer:modified", { layerId: id });
@@ -226,6 +242,42 @@ var LayerManager = class {
226
242
  this.onPropertyChanged?.();
227
243
  return true;
228
244
  }
245
+ /**
246
+ * Attach (or clear) the object drawn in place of a layer's own object. The
247
+ * proxy tracks the layer's stacking position, visibility and opacity, and is
248
+ * removed with the layer — it must never outlive or drift from its source.
249
+ */
250
+ setRenderProxy(id, proxy) {
251
+ const layer = this.get(id);
252
+ if (!layer || layer.renderProxy === proxy) return;
253
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
254
+ layer.renderProxy = proxy;
255
+ if (proxy) {
256
+ proxy._layerId = id;
257
+ proxy.set({
258
+ visible: layer.visible,
259
+ opacity: layer.opacity,
260
+ selectable: !layer.locked,
261
+ evented: !layer.locked
262
+ });
263
+ this.canvas.add(proxy);
264
+ this.syncZOrder();
265
+ }
266
+ this.canvas.requestRenderAll();
267
+ this.emitChanged();
268
+ }
269
+ /**
270
+ * Re-stack every canvas object to match layer order, keeping each proxy
271
+ * directly above the source it stands in for. Canvas indices can't be derived
272
+ * from layer indices once proxies are in the array, so the order is rebuilt
273
+ * front-to-back instead of computed.
274
+ */
275
+ syncZOrder() {
276
+ for (const layer of this.layers) {
277
+ this.canvas.bringObjectToFront(layer.fabricObject);
278
+ if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
279
+ }
280
+ }
229
281
  reorder(id, newIndex) {
230
282
  const oldIndex = this.layers.findIndex((l) => l.id === id);
231
283
  if (oldIndex === -1) return false;
@@ -234,9 +286,7 @@ var LayerManager = class {
234
286
  if (oldIndex === clamped) return false;
235
287
  const [layer] = this.layers.splice(oldIndex, 1);
236
288
  this.layers.splice(clamped, 0, layer);
237
- this.layers.forEach((l, i) => {
238
- this.canvas.moveObjectTo(l.fabricObject, i);
239
- });
289
+ this.syncZOrder();
240
290
  this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
241
291
  this.emitChanged();
242
292
  this.onPropertyChanged?.();
@@ -248,7 +298,7 @@ var LayerManager = class {
248
298
  } else {
249
299
  const layer = this.get(id);
250
300
  if (layer) {
251
- this.canvas.setActiveObject(layer.fabricObject);
301
+ this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
252
302
  }
253
303
  }
254
304
  this.canvas.requestRenderAll();
@@ -273,6 +323,7 @@ var LayerManager = class {
273
323
  if (layer.visible === visible) return;
274
324
  layer.visible = visible;
275
325
  layer.fabricObject.visible = visible;
326
+ if (layer.renderProxy) layer.renderProxy.visible = visible;
276
327
  this.canvas.requestRenderAll();
277
328
  this.emitChanged();
278
329
  this.onPropertyChanged?.();
@@ -282,8 +333,9 @@ var LayerManager = class {
282
333
  if (!layer) return;
283
334
  if (layer.locked === locked) return;
284
335
  layer.locked = locked;
285
- layer.fabricObject.selectable = !locked;
286
- layer.fabricObject.evented = !locked;
336
+ const target = layer.renderProxy ?? layer.fabricObject;
337
+ target.selectable = !locked;
338
+ target.evented = !locked;
287
339
  this.canvas.requestRenderAll();
288
340
  this.emitChanged();
289
341
  this.onPropertyChanged?.();
@@ -294,7 +346,8 @@ var LayerManager = class {
294
346
  const next = Math.max(0, Math.min(1, opacity));
295
347
  if (layer.opacity === next) return;
296
348
  layer.opacity = next;
297
- layer.fabricObject.opacity = next;
349
+ if (layer.renderProxy) layer.renderProxy.opacity = next;
350
+ else layer.fabricObject.opacity = next;
298
351
  this.canvas.requestRenderAll();
299
352
  this.emitChanged();
300
353
  this.onPropertyChanged?.();
@@ -342,6 +395,7 @@ var LayerManager = class {
342
395
  clear() {
343
396
  for (const layer of this.layers) {
344
397
  this.canvas.remove(layer.fabricObject);
398
+ if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
345
399
  }
346
400
  this.layers = [];
347
401
  this.emitChanged();
@@ -875,199 +929,536 @@ var CropController = class {
875
929
  };
876
930
  var STROKE2 = "#22c55e";
877
931
 
878
- // src/pattern.ts
932
+ // src/pattern/pattern-manager.ts
933
+ var import_fabric3 = require("fabric");
934
+
935
+ // src/pattern/tiled-pattern-object.ts
879
936
  var import_fabric2 = require("fabric");
937
+
938
+ // src/pattern/tile-geometry.ts
880
939
  var MAX_TILES_PER_AXIS = 200;
940
+ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
941
+ const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
942
+ const radius = cornerRadius(anchor, targetW, targetH);
943
+ const span = radius * 2;
944
+ const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
945
+ const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
946
+ const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
947
+ const cols = Math.ceil(span / tileW) + 2;
948
+ const rows = Math.ceil(span / tileH) + 2;
949
+ const halfCols = Math.ceil(cols / 2);
950
+ const halfRows = Math.ceil(rows / 2);
951
+ const shiftX = tileW * (clampOffset(config.offsetX) / 100);
952
+ const shiftY = tileH * (clampOffset(config.offsetY) / 100);
953
+ const placements = [];
954
+ for (let j = -halfRows; j <= halfRows; j++) {
955
+ for (let i = -halfCols; i <= halfCols; i++) {
956
+ let x = i * tileW + shiftX;
957
+ let y = j * tileH + shiftY;
958
+ if (config.mode === "brick-horizontal" && mod2(j) === 1) {
959
+ x += tileW * (config.horizontalOffset / 100);
960
+ } else if (config.mode === "brick-vertical" && mod2(i) === 1) {
961
+ y += tileH * (config.horizontalOffset / 100);
962
+ }
963
+ const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
964
+ placements.push({ x, y, rotation });
965
+ }
966
+ }
967
+ return placements;
968
+ }
969
+ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
970
+ const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
971
+ ctx.save();
972
+ ctx.translate(anchor.x, anchor.y);
973
+ ctx.rotate(config.angle * Math.PI / 180);
974
+ for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
975
+ ctx.save();
976
+ ctx.translate(tile.x, tile.y);
977
+ ctx.rotate(tile.rotation);
978
+ ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
979
+ ctx.restore();
980
+ }
981
+ ctx.restore();
982
+ }
983
+ function cornerRadius(origin, w, h) {
984
+ const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
985
+ const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
986
+ return Math.sqrt(dx * dx + dy * dy);
987
+ }
988
+ function clampOffset(value) {
989
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
990
+ return Math.max(-100, Math.min(100, value));
991
+ }
992
+ function mod2(n) {
993
+ return (n % 2 + 2) % 2;
994
+ }
995
+
996
+ // src/pattern/tiled-pattern-object.ts
997
+ var MAX_SNAPSHOT_PIXELS = 16e6;
998
+ var MAX_SNAPSHOT_SCALE = 8;
999
+ var SNAPSHOT_SHRINK_FACTOR = 2;
1000
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
1001
+ static type = "TiledPattern";
1002
+ /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
1003
+ source;
1004
+ config;
1005
+ /** The print area this pattern fills, in canvas units. */
1006
+ area;
1007
+ snapshotEl = null;
1008
+ snapshotScale = 0;
1009
+ constructor(source, config, area) {
1010
+ super({
1011
+ // Centre origin: the box tracks a tile, and a tile is placed by its centre.
1012
+ originX: "center",
1013
+ originY: "center",
1014
+ objectCaching: false,
1015
+ // Side handles would imply a non-uniform tile scale; the config has one.
1016
+ lockScalingFlip: true
1017
+ });
1018
+ this.source = source;
1019
+ this.config = config;
1020
+ this.area = area;
1021
+ this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
1022
+ this.syncBox();
1023
+ }
1024
+ /** Swap in a new config and re-fit the box to the tile it now describes. */
1025
+ setConfig(config) {
1026
+ this.config = config;
1027
+ this.syncBox();
1028
+ }
1029
+ /** Re-fit to a new print area (canvas resize). */
1030
+ setArea(area) {
1031
+ this.area = area;
1032
+ this.invalidate();
1033
+ this.syncBox();
1034
+ }
1035
+ /** Drop the cached source snapshot (the source was edited). */
1036
+ invalidate() {
1037
+ this.snapshotEl = null;
1038
+ this.snapshotScale = 0;
1039
+ this.dirty = true;
1040
+ }
1041
+ /** Free the offscreen snapshot. */
1042
+ dispose() {
1043
+ this.snapshotEl = null;
1044
+ this.snapshotScale = 0;
1045
+ }
1046
+ /**
1047
+ * Put the box back on the anchor tile: the tile's size, the pattern angle, and
1048
+ * the grid origin displaced by the configured phase shift. Resets any live
1049
+ * gesture scale, so it must run only once that gesture has been folded in.
1050
+ */
1051
+ syncBox() {
1052
+ const { tileW, tileH } = this.baseTile();
1053
+ const origin = this.source.getCenterPoint();
1054
+ const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
1055
+ this.set({
1056
+ left: anchor.x,
1057
+ top: anchor.y,
1058
+ width: tileW,
1059
+ height: tileH,
1060
+ scaleX: 1,
1061
+ scaleY: 1,
1062
+ angle: this.config.angle
1063
+ });
1064
+ this.setCoords();
1065
+ this.dirty = true;
1066
+ }
1067
+ /** Grid rotation in play right now — the live handle during a rotate gesture. */
1068
+ liveAngle() {
1069
+ return this.angle ?? 0;
1070
+ }
1071
+ /** Tile scale in play right now, as a config percentage. */
1072
+ liveScale() {
1073
+ return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
1074
+ }
1075
+ /**
1076
+ * Where the source's centre would have to sit for the box to stay put — i.e.
1077
+ * the grid origin implied by a drag. `PatternManager` moves the source there.
1078
+ */
1079
+ liveOrigin() {
1080
+ const { tileW, tileH } = this.liveTile();
1081
+ const centre = this.getCenterPoint();
1082
+ const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1083
+ return new import_fabric2.Point(centre.x - shift.x, centre.y - shift.y);
1084
+ }
1085
+ _render(ctx) {
1086
+ const { width, height } = this.area;
1087
+ if (width <= 0 || height <= 0) return;
1088
+ const { tileW, tileH } = this.liveTile();
1089
+ const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
1090
+ if (!snapshot) return;
1091
+ const centre = this.getCenterPoint();
1092
+ const angle = this.liveAngle();
1093
+ ctx.save();
1094
+ ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
1095
+ ctx.rotate(-angle * Math.PI / 180);
1096
+ ctx.translate(-centre.x, -centre.y);
1097
+ const shift = this.shiftVector(tileW, tileH, angle);
1098
+ const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1099
+ const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1100
+ drawTiles(ctx, snapshot, config, width, height, tileW, tileH, origin);
1101
+ ctx.restore();
1102
+ }
1103
+ /** Raster fallback for SVG export — one `<image>` covering the print area. */
1104
+ _toSVG() {
1105
+ const { width, height } = this.area;
1106
+ if (width <= 0 || height <= 0) return [];
1107
+ const el = document.createElement("canvas");
1108
+ el.width = Math.max(1, Math.round(width));
1109
+ el.height = Math.max(1, Math.round(height));
1110
+ const ctx = el.getContext("2d");
1111
+ if (!ctx) return [];
1112
+ const { tileW, tileH } = this.liveTile();
1113
+ const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
1114
+ if (!snapshot) return [];
1115
+ const centre = this.getCenterPoint();
1116
+ const angle = this.liveAngle();
1117
+ const shift = this.shiftVector(tileW, tileH, angle);
1118
+ drawTiles(
1119
+ ctx,
1120
+ snapshot,
1121
+ { ...this.config, angle, offsetX: 0, offsetY: 0 },
1122
+ width,
1123
+ height,
1124
+ tileW,
1125
+ tileH,
1126
+ { x: centre.x - shift.x, y: centre.y - shift.y }
1127
+ );
1128
+ return [
1129
+ `<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
1130
+ `<image x="0" y="0" width="${width}" height="${height}" `,
1131
+ `xlink:href="${el.toDataURL("image/png")}"></image></g>
1132
+ `
1133
+ ];
1134
+ }
1135
+ /**
1136
+ * The proxy holds a live reference to its source, which would make a
1137
+ * serialized canvas circular. Nothing persists this object (only layers are
1138
+ * serialized) — this keeps an accidental `canvas.toObject()` from throwing.
1139
+ */
1140
+ toObject() {
1141
+ const plain = super.toObject();
1142
+ delete plain.source;
1143
+ return plain;
1144
+ }
1145
+ /**
1146
+ * Fabric's generic clone round-trips through `toObject()` + the class
1147
+ * registry, which cannot carry a live source reference. Export paths clone
1148
+ * every canvas object, so without this a print export would silently lose the
1149
+ * tiling. The copy shares the source (it only ever reads from it).
1150
+ */
1151
+ clone() {
1152
+ const copy = new _TiledPatternObject(this.source, this.config, this.area);
1153
+ copy.set({
1154
+ left: this.left,
1155
+ top: this.top,
1156
+ angle: this.angle,
1157
+ scaleX: this.scaleX,
1158
+ scaleY: this.scaleY,
1159
+ width: this.width,
1160
+ height: this.height,
1161
+ visible: this.visible,
1162
+ opacity: this.opacity
1163
+ });
1164
+ return Promise.resolve(copy);
1165
+ }
1166
+ /** The source's on-canvas width, before the tile scale. */
1167
+ baseW() {
1168
+ return Math.max(1, this.source.getBoundingRect().width);
1169
+ }
1170
+ /** Tile size from the config alone, ignoring any in-flight gesture. */
1171
+ baseTile() {
1172
+ const rect = this.source.getBoundingRect();
1173
+ const scale = Math.max(1, this.config.scale ?? 100) / 100;
1174
+ const floor = this.tileFloor();
1175
+ return {
1176
+ tileW: Math.max(floor, rect.width * scale),
1177
+ tileH: Math.max(floor, rect.height * scale)
1178
+ };
1179
+ }
1180
+ /** Tile size as drawn right now, including a live scale gesture. */
1181
+ liveTile() {
1182
+ const base = this.baseTile();
1183
+ const floor = this.tileFloor();
1184
+ return {
1185
+ tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
1186
+ tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
1187
+ };
1188
+ }
1189
+ /** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
1190
+ tileFloor() {
1191
+ const { width, height } = this.area;
1192
+ return Math.max(2, Math.sqrt(width * width + height * height) / 200);
1193
+ }
1194
+ /** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
1195
+ shiftVector(tileW, tileH, angle) {
1196
+ const dx = tileW * (clampPercent(this.config.offsetX) / 100);
1197
+ const dy = tileH * (clampPercent(this.config.offsetY) / 100);
1198
+ const radians = angle * Math.PI / 180;
1199
+ const cos = Math.cos(radians);
1200
+ const sin = Math.sin(radians);
1201
+ return new import_fabric2.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1202
+ }
1203
+ anchorFromOrigin(origin, tileW, tileH, angle) {
1204
+ const shift = this.shiftVector(tileW, tileH, angle);
1205
+ return new import_fabric2.Point(origin.x + shift.x, origin.y + shift.y);
1206
+ }
1207
+ /**
1208
+ * Snapshot the source at (at least) `scale`, reusing the cached one while it
1209
+ * is still sharp enough and the source has not changed.
1210
+ */
1211
+ ensureSnapshot(scale) {
1212
+ const wanted = this.clampScale(scale);
1213
+ const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
1214
+ if (!stale) return this.snapshotEl;
1215
+ const source = this.source;
1216
+ const opacity = source.opacity;
1217
+ source.opacity = 1;
1218
+ try {
1219
+ const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
1220
+ if (!el.width || !el.height) return null;
1221
+ this.snapshotEl = el;
1222
+ this.snapshotScale = wanted;
1223
+ } catch {
1224
+ return this.snapshotEl;
1225
+ } finally {
1226
+ source.opacity = opacity;
1227
+ source.dirty = false;
1228
+ }
1229
+ return this.snapshotEl;
1230
+ }
1231
+ /** Bound the snapshot by both a linear scale and a total pixel budget. */
1232
+ clampScale(scale) {
1233
+ const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
1234
+ const rect = this.source.getBoundingRect();
1235
+ const area = Math.max(1, rect.width * rect.height);
1236
+ const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
1237
+ return Math.max(0.05, Math.min(requested, budgeted));
1238
+ }
1239
+ };
1240
+ function clampPercent(value) {
1241
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1242
+ return Math.max(-100, Math.min(100, value));
1243
+ }
1244
+ function contextScale(ctx) {
1245
+ if (typeof ctx.getTransform !== "function") return 1;
1246
+ try {
1247
+ const t = ctx.getTransform();
1248
+ return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
1249
+ } catch {
1250
+ return 1;
1251
+ }
1252
+ }
1253
+
1254
+ // src/pattern/pattern-manager.ts
1255
+ var MIN_TILE_SCALE = 10;
1256
+ var MAX_TILE_SCALE = 300;
881
1257
  var PatternManager = class {
882
- constructor(canvas, layers, history, events, sourceResolver) {
1258
+ constructor(canvas, layers, history, events) {
883
1259
  this.canvas = canvas;
884
1260
  this.layers = layers;
885
1261
  this.history = history;
886
1262
  this.events = events;
887
- this.sourceResolver = sourceResolver;
1263
+ this.canvas.on("object:modified", this.onObjectModified);
1264
+ this.canvas.on("text:changed", this.onSourceModified);
1265
+ this.events.on("layer:removed", this.onLayerRemoved);
888
1266
  }
889
1267
  canvas;
890
1268
  layers;
891
1269
  history;
892
1270
  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();
1271
+ proxies = /* @__PURE__ */ new Map();
1272
+ onObjectModified = (event) => {
1273
+ const target = event.target;
1274
+ if (target instanceof TiledPatternObject) this.commitGesture(target);
1275
+ else this.invalidateFor(target);
1276
+ };
1277
+ onSourceModified = (event) => {
1278
+ this.invalidateFor(event.target);
1279
+ };
1280
+ onLayerRemoved = ({ layerId }) => {
1281
+ const proxy = this.proxies.get(layerId);
1282
+ if (!proxy) return;
1283
+ proxy.dispose();
1284
+ this.proxies.delete(layerId);
1285
+ };
899
1286
  isPattern(layerId) {
900
1287
  return !!this.layers.get(layerId)?.meta.pattern;
901
1288
  }
902
1289
  getConfig(layerId) {
903
1290
  return this.layers.get(layerId)?.meta.pattern?.config ?? null;
904
1291
  }
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
- };
1292
+ /** Turn a layer into a repeating pattern, or update an existing one. */
1293
+ async apply(layerId, config) {
1294
+ const layer = this.layers.get(layerId);
1295
+ if (!layer) return;
1296
+ try {
1297
+ this.layers.setMeta(layerId, { pattern: { config } });
1298
+ const proxy = this.proxies.get(layerId);
1299
+ if (proxy) {
1300
+ proxy.setConfig(config);
1301
+ this.canvas.requestRenderAll();
931
1302
  } 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;
1303
+ this.attach(layer, config);
939
1304
  }
940
1305
  this.history.save();
941
- }).catch((error) => {
942
- this.events.emit("error", { message: "Failed to apply image pattern", error });
1306
+ } catch (error) {
1307
+ this.events.emit("error", { message: "Failed to apply pattern", error });
943
1308
  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
1309
  }
972
- if (changed) this.canvas.requestRenderAll();
973
1310
  }
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
1311
+ /** Drop the tiling and show the source again. */
1312
+ async disable(layerId) {
1313
+ const layer = this.layers.get(layerId);
1314
+ if (!layer?.meta.pattern) return;
1315
+ try {
1316
+ this.detach(layerId);
1317
+ restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
1318
+ layer.fabricObject.set({
1319
+ opacity: layer.opacity,
1320
+ selectable: !layer.locked,
1321
+ evented: !layer.locked
992
1322
  });
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;
1323
+ this.layers.setMeta(layerId, { pattern: void 0 });
997
1324
  this.canvas.requestRenderAll();
998
1325
  this.history.save();
999
- }).catch((error) => {
1000
- this.events.emit("error", { message: "Failed to clear image pattern", error });
1326
+ } catch (error) {
1327
+ this.events.emit("error", { message: "Failed to clear pattern", error });
1001
1328
  throw error;
1002
- });
1329
+ }
1003
1330
  }
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;
1331
+ /**
1332
+ * Rebuild every proxy after a state restore, migrating any layer that was
1333
+ * saved by the old bake-into-the-layer engine.
1334
+ */
1335
+ async rehydrateAll() {
1336
+ this.clearProxies();
1337
+ for (const layer of this.layers.getAll()) {
1338
+ const state = layer.meta.pattern;
1339
+ if (!state) continue;
1340
+ try {
1341
+ if (isLegacyState(state)) {
1342
+ await unbakeLegacyLayer(layer, state);
1343
+ this.layers.setMeta(layer.id, { pattern: { config: state.config } });
1344
+ }
1345
+ this.attach(layer, state.config);
1346
+ } catch (error) {
1347
+ this.events.emit("error", { message: "Failed to restore pattern layer", error });
1348
+ }
1349
+ }
1350
+ this.canvas.requestRenderAll();
1351
+ }
1352
+ /** Re-fit every proxy to the print area (canvas resize). */
1353
+ syncArea() {
1354
+ const area = this.area();
1355
+ for (const proxy of this.proxies.values()) proxy.setArea(area);
1356
+ if (this.proxies.size > 0) this.canvas.requestRenderAll();
1357
+ }
1358
+ /** Drop a layer's cached source snapshot (its content changed). */
1359
+ invalidate(layerId) {
1360
+ const proxy = this.proxies.get(layerId);
1361
+ if (!proxy) return;
1362
+ proxy.invalidate();
1363
+ proxy.syncBox();
1364
+ this.canvas.requestRenderAll();
1013
1365
  }
1014
- async renderLayer(layer) {
1366
+ /** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
1367
+ attachTo(layer) {
1015
1368
  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
1033
- );
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();
1369
+ if (!state || this.proxies.has(layer.id)) return;
1370
+ this.attach(layer, state.config);
1371
+ }
1372
+ dispose() {
1373
+ this.canvas.off("object:modified", this.onObjectModified);
1374
+ this.canvas.off("text:changed", this.onSourceModified);
1375
+ this.events.off("layer:removed", this.onLayerRemoved);
1376
+ this.clearProxies();
1377
+ }
1378
+ /** Release every proxy and the offscreen snapshot it holds. */
1379
+ clearProxies() {
1380
+ for (const proxy of this.proxies.values()) proxy.dispose();
1381
+ this.proxies.clear();
1382
+ }
1383
+ attach(layer, config) {
1384
+ const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
1385
+ const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
1386
+ layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
1387
+ this.proxies.set(layer.id, proxy);
1388
+ this.layers.setRenderProxy(layer.id, proxy);
1389
+ if (wasActive) this.canvas.setActiveObject(proxy);
1390
+ this.canvas.requestRenderAll();
1391
+ }
1392
+ /**
1393
+ * Fold a finished drag / scale / rotate on the proxy back into the pattern:
1394
+ * position becomes the grid origin (carried by the source), scale becomes the
1395
+ * tile scale, rotation becomes the pattern angle. Read the live transform
1396
+ * first — writing the config re-fits the box and destroys it.
1397
+ */
1398
+ commitGesture(proxy) {
1399
+ const layer = this.layers.findByObject(proxy);
1400
+ const state = layer?.meta.pattern;
1401
+ if (!layer || !state) return;
1402
+ const origin = proxy.liveOrigin();
1403
+ const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
1404
+ const angle = normalizeAngle(proxy.liveAngle());
1405
+ layer.fabricObject.setPositionByOrigin(origin, "center", "center");
1406
+ layer.fabricObject.setCoords();
1407
+ const config = { ...state.config, scale, angle };
1408
+ this.layers.setMeta(layer.id, { pattern: { ...state, config } });
1409
+ proxy.setConfig(config);
1049
1410
  this.canvas.requestRenderAll();
1411
+ this.history.save();
1412
+ }
1413
+ area() {
1414
+ return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
1415
+ }
1416
+ detach(layerId) {
1417
+ const proxy = this.proxies.get(layerId);
1418
+ if (!proxy) {
1419
+ this.layers.setRenderProxy(layerId, null);
1420
+ return;
1421
+ }
1422
+ this.layers.setRenderProxy(layerId, null);
1423
+ proxy.dispose();
1424
+ this.proxies.delete(layerId);
1425
+ }
1426
+ invalidateFor(target) {
1427
+ if (!target) return;
1428
+ const layer = this.layers.findByObject(target);
1429
+ if (layer) this.invalidate(layer.id);
1050
1430
  }
1051
1431
  };
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
- };
1432
+ function clamp2(value, min, max) {
1433
+ if (!Number.isFinite(value)) return min;
1434
+ return Math.max(min, Math.min(max, value));
1061
1435
  }
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
1436
+ function normalizeAngle(angle) {
1437
+ if (!Number.isFinite(angle)) return 0;
1438
+ const wrapped = (angle % 360 + 360) % 360;
1439
+ return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
1440
+ }
1441
+ function isLegacyState(state) {
1442
+ return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
1443
+ }
1444
+ async function unbakeLegacyLayer(layer, state) {
1445
+ const image = layer.fabricObject;
1446
+ if (typeof image.setSrc !== "function" || !state.original) return;
1447
+ await image.setSrc(state.originalSrc);
1448
+ image.set({
1449
+ left: state.original.left,
1450
+ top: state.original.top,
1451
+ scaleX: state.original.scaleX,
1452
+ scaleY: state.original.scaleY,
1453
+ width: state.original.width,
1454
+ height: state.original.height,
1455
+ cropX: state.original.cropX,
1456
+ cropY: state.original.cropY,
1457
+ angle: state.original.angle
1070
1458
  });
1459
+ image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
1460
+ restoreLocks(image, state.originalLocks);
1461
+ image.setCoords();
1071
1462
  }
1072
1463
  function restoreLocks(obj, locks) {
1073
1464
  obj.set(
@@ -1081,124 +1472,9 @@ function restoreLocks(obj, locks) {
1081
1472
  }
1082
1473
  );
1083
1474
  }
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
1475
 
1200
1476
  // src/text-curve.ts
1201
- var import_fabric3 = require("fabric");
1477
+ var import_fabric4 = require("fabric");
1202
1478
  var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
1203
1479
  var MIN_ARC = 0.5;
1204
1480
  var FULL_CIRCLE_ARC = 99.5;
@@ -1321,7 +1597,7 @@ var TextCurveManager = class {
1321
1597
  if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
1322
1598
  text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
1323
1599
  text.set({
1324
- path: new import_fabric3.Path(curve.data, { visible: false, objectCaching: false }),
1600
+ path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
1325
1601
  pathAlign: "center",
1326
1602
  pathSide: "left",
1327
1603
  pathStartOffset: Math.max(0, (curve.length - run) / 2)
@@ -1363,7 +1639,7 @@ var TextCurveManager = class {
1363
1639
  };
1364
1640
 
1365
1641
  // src/mask-presets/manager.ts
1366
- var import_fabric4 = require("fabric");
1642
+ var import_fabric5 = require("fabric");
1367
1643
 
1368
1644
  // src/mask-presets/shapes.ts
1369
1645
  var SHAPE_MASK_IDS = [
@@ -1630,13 +1906,13 @@ var MaskPresetManager = class {
1630
1906
  objectCaching: false
1631
1907
  };
1632
1908
  if (isShapeMaskId(id)) {
1633
- return new import_fabric4.Path(shapeMaskPathData(id), {
1909
+ return new import_fabric5.Path(shapeMaskPathData(id), {
1634
1910
  ...shared,
1635
1911
  scaleX: width / SHAPE_MASK_BOX,
1636
1912
  scaleY: height / SHAPE_MASK_BOX
1637
1913
  });
1638
1914
  }
1639
- return new import_fabric4.FabricImage(renderTextureMask(id), {
1915
+ return new import_fabric5.FabricImage(renderTextureMask(id), {
1640
1916
  ...shared,
1641
1917
  scaleX: width / TEXTURE_MASK_SIZE,
1642
1918
  scaleY: height / TEXTURE_MASK_SIZE
@@ -1645,7 +1921,7 @@ var MaskPresetManager = class {
1645
1921
  };
1646
1922
 
1647
1923
  // src/shadow.ts
1648
- var import_fabric5 = require("fabric");
1924
+ var import_fabric6 = require("fabric");
1649
1925
 
1650
1926
  // src/utils/color.ts
1651
1927
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -1683,7 +1959,7 @@ function applyLayerShadow(object, config) {
1683
1959
  }
1684
1960
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
1685
1961
  object.set({
1686
- shadow: new import_fabric5.Shadow({
1962
+ shadow: new import_fabric6.Shadow({
1687
1963
  color,
1688
1964
  blur: Math.max(0, next.blur),
1689
1965
  offsetX: next.offsetX,
@@ -1797,7 +2073,7 @@ var UnitConverter = class {
1797
2073
  };
1798
2074
 
1799
2075
  // src/serialization.ts
1800
- var import_fabric6 = require("fabric");
2076
+ var import_fabric7 = require("fabric");
1801
2077
  var VERSION = "2.0.0";
1802
2078
  function serializeEditor(editor) {
1803
2079
  return {
@@ -1833,7 +2109,7 @@ async function deserializeEditor(editor, state) {
1833
2109
  }
1834
2110
  const staged = await Promise.all(
1835
2111
  state.layers.map(async (serialized) => {
1836
- const fabricObject = (await import_fabric6.util.enlivenObjects([serialized.fabricObject]))[0];
2112
+ const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
1837
2113
  if (!fabricObject) {
1838
2114
  const source = serialized.fabricObject.src;
1839
2115
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -1844,7 +2120,7 @@ async function deserializeEditor(editor, state) {
1844
2120
  return { serialized, fabricObject };
1845
2121
  })
1846
2122
  );
1847
- const stagedBackground = state.backgroundImage ? (await import_fabric6.util.enlivenObjects([state.backgroundImage]))[0] : null;
2123
+ const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
1848
2124
  if (state.backgroundImage && !stagedBackground) {
1849
2125
  const source = state.backgroundImage.src;
1850
2126
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -1891,7 +2167,7 @@ function restoreLayer(editor, serialized, fabricObject) {
1891
2167
  }
1892
2168
 
1893
2169
  // src/export.ts
1894
- var import_fabric7 = require("fabric");
2170
+ var import_fabric8 = require("fabric");
1895
2171
 
1896
2172
  // src/displacement.ts
1897
2173
  var CHANNEL_INDEX = {
@@ -1985,7 +2261,7 @@ async function exportPNG(canvas, options = {}) {
1985
2261
  }
1986
2262
  async function exportIsolatedPNG(source, objects, options = {}) {
1987
2263
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
1988
- const canvas = new import_fabric7.StaticCanvas(element, {
2264
+ const canvas = new import_fabric8.StaticCanvas(element, {
1989
2265
  width: options.width ?? source.getWidth(),
1990
2266
  height: options.height ?? source.getHeight(),
1991
2267
  backgroundColor: options.backgroundColor || void 0
@@ -2015,7 +2291,7 @@ async function exportPrintArea(source, area, options = {}) {
2015
2291
  throw new Error("Print area does not overlap the canvas");
2016
2292
  }
2017
2293
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2018
- const canvas = new import_fabric7.StaticCanvas(element, { width, height });
2294
+ const canvas = new import_fabric8.StaticCanvas(element, { width, height });
2019
2295
  try {
2020
2296
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2021
2297
  if (clones.length) canvas.add(...clones);
@@ -2455,7 +2731,7 @@ var ProjectManager = class {
2455
2731
  };
2456
2732
 
2457
2733
  // src/mask.ts
2458
- var import_fabric8 = require("fabric");
2734
+ var import_fabric9 = require("fabric");
2459
2735
  var MaskRefinementError = class extends Error {
2460
2736
  constructor(code, message, cause) {
2461
2737
  super(message);
@@ -2487,7 +2763,7 @@ var MaskController = class {
2487
2763
  throw new Error("Mask dimensions must be positive integers");
2488
2764
  }
2489
2765
  const backing = this.makeCanvas(width, height);
2490
- const image = new import_fabric8.FabricImage(backing, {
2766
+ const image = new import_fabric9.FabricImage(backing, {
2491
2767
  left: 0,
2492
2768
  top: 0,
2493
2769
  originX: "left",
@@ -2712,6 +2988,737 @@ var MaskController = class {
2712
2988
  }
2713
2989
  };
2714
2990
 
2991
+ // src/masks/manager.ts
2992
+ var import_fabric16 = require("fabric");
2993
+
2994
+ // src/masks/compose.ts
2995
+ var import_fabric10 = require("fabric");
2996
+ var MODE_OPERATION = {
2997
+ add: "source-over",
2998
+ subtract: "destination-out",
2999
+ intersect: "destination-in"
3000
+ };
3001
+ function neutralize(child) {
3002
+ child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3003
+ }
3004
+ function baseRect(box) {
3005
+ return new import_fabric10.Rect({
3006
+ left: box.left,
3007
+ top: box.top,
3008
+ width: Math.max(1, box.width),
3009
+ height: Math.max(1, box.height),
3010
+ originX: "left",
3011
+ originY: "top",
3012
+ fill: "#000000",
3013
+ objectCaching: false
3014
+ });
3015
+ }
3016
+ function composeMaskGroup(children, entries, options) {
3017
+ if (children.length === 0) return void 0;
3018
+ children.forEach((child, index) => {
3019
+ const entry = entries[index];
3020
+ child.set({ objectCaching: false });
3021
+ if (!entry || !entry.visible) {
3022
+ neutralize(child);
3023
+ return;
3024
+ }
3025
+ child.set({
3026
+ opacity: entry.opacity,
3027
+ globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
3028
+ });
3029
+ });
3030
+ const first = entries.find((entry) => entry.visible);
3031
+ const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3032
+ return new import_fabric10.Group(withBase, {
3033
+ absolutePositioned: options.absolute,
3034
+ // Cached, so the children's compositing operations resolve against each
3035
+ // other instead of against the page underneath the mask.
3036
+ objectCaching: true,
3037
+ subTargetCheck: false,
3038
+ interactive: false
3039
+ });
3040
+ }
3041
+ function needsAbsoluteSpace(entries) {
3042
+ return entries.some((entry) => !entry.linked);
3043
+ }
3044
+
3045
+ // src/masks/edit.ts
3046
+ var import_fabric12 = require("fabric");
3047
+
3048
+ // src/masks/space.ts
3049
+ var import_fabric11 = require("fabric");
3050
+ function matrixOf(object) {
3051
+ return object.calcTransformMatrix();
3052
+ }
3053
+ function applyMatrix(object, matrix) {
3054
+ const decomposed = import_fabric11.util.qrDecompose(matrix);
3055
+ object.set({
3056
+ flipX: false,
3057
+ flipY: false,
3058
+ originX: "center",
3059
+ originY: "center",
3060
+ left: decomposed.translateX,
3061
+ top: decomposed.translateY,
3062
+ scaleX: decomposed.scaleX,
3063
+ scaleY: decomposed.scaleY,
3064
+ angle: decomposed.angle,
3065
+ skewX: decomposed.skewX,
3066
+ skewY: 0
3067
+ });
3068
+ object.setCoords();
3069
+ }
3070
+ function toCanvasSpace(object, host) {
3071
+ applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3072
+ }
3073
+ function toHostSpace(object, host) {
3074
+ applyMatrix(
3075
+ object,
3076
+ import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object))
3077
+ );
3078
+ }
3079
+ function relativeMatrix(object, host) {
3080
+ return import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object));
3081
+ }
3082
+ function applyRelativeMatrix(object, host, rel) {
3083
+ applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), rel));
3084
+ }
3085
+ function asObject(clip) {
3086
+ return clip;
3087
+ }
3088
+ function toMatrix(values) {
3089
+ if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3090
+ return null;
3091
+ return [values[0], values[1], values[2], values[3], values[4], values[5]];
3092
+ }
3093
+ function fitToBox(object, box, zoom = 1) {
3094
+ const width = Math.max(1, box.width) * zoom;
3095
+ const height = Math.max(1, box.height) * zoom;
3096
+ object.set({
3097
+ originX: "center",
3098
+ originY: "center",
3099
+ angle: 0,
3100
+ skewX: 0,
3101
+ skewY: 0,
3102
+ left: box.left + box.width / 2,
3103
+ top: box.top + box.height / 2,
3104
+ scaleX: width / Math.max(1, object.width ?? 1),
3105
+ scaleY: height / Math.max(1, object.height ?? 1)
3106
+ });
3107
+ object.setCoords();
3108
+ }
3109
+ function unwrapGroup(group) {
3110
+ const children = group.removeAll();
3111
+ for (const child of children) child.setCoords();
3112
+ return children;
3113
+ }
3114
+
3115
+ // src/masks/edit.ts
3116
+ var MaskEditController = class {
3117
+ constructor(canvas, onCommit) {
3118
+ this.canvas = canvas;
3119
+ this.onCommit = onCommit;
3120
+ }
3121
+ canvas;
3122
+ onCommit;
3123
+ handle = null;
3124
+ editing = null;
3125
+ child = null;
3126
+ group = null;
3127
+ active() {
3128
+ return this.editing;
3129
+ }
3130
+ /** Put a handle on the canvas for `child`, an object inside `group`. */
3131
+ async begin(edit, group, child) {
3132
+ this.end();
3133
+ const handle = await child.clone();
3134
+ applyMatrix(handle, matrixOf(child));
3135
+ handle.set({
3136
+ // Outline only: the mask's own fill would sit over the artwork it is
3137
+ // supposed to be revealing.
3138
+ fill: "rgba(0,0,0,0.001)",
3139
+ stroke: "#e0b055",
3140
+ strokeWidth: 2,
3141
+ strokeDashArray: [6, 4],
3142
+ strokeUniform: true,
3143
+ opacity: 1,
3144
+ selectable: true,
3145
+ evented: true,
3146
+ hasControls: true,
3147
+ hasBorders: true,
3148
+ objectCaching: false,
3149
+ excludeFromExport: true
3150
+ });
3151
+ this.handle = handle;
3152
+ this.editing = edit;
3153
+ this.child = child;
3154
+ this.group = group;
3155
+ this.canvas.add(handle);
3156
+ this.canvas.setActiveObject(handle);
3157
+ this.canvas.on("object:moving", this.onTransform);
3158
+ this.canvas.on("object:scaling", this.onTransform);
3159
+ this.canvas.on("object:rotating", this.onTransform);
3160
+ this.canvas.on("object:modified", this.onModified);
3161
+ this.canvas.requestRenderAll();
3162
+ return true;
3163
+ }
3164
+ /** Take the handle down. The geometry it wrote is already in the clip. */
3165
+ end() {
3166
+ if (!this.handle) return;
3167
+ this.canvas.off("object:moving", this.onTransform);
3168
+ this.canvas.off("object:scaling", this.onTransform);
3169
+ this.canvas.off("object:rotating", this.onTransform);
3170
+ this.canvas.off("object:modified", this.onModified);
3171
+ if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
3172
+ this.canvas.remove(this.handle);
3173
+ this.handle = null;
3174
+ this.editing = null;
3175
+ this.child = null;
3176
+ this.group = null;
3177
+ this.canvas.requestRenderAll();
3178
+ }
3179
+ dispose() {
3180
+ this.end();
3181
+ }
3182
+ onTransform = (event) => {
3183
+ if (!this.handle || event.target !== this.handle) return;
3184
+ this.write();
3185
+ };
3186
+ onModified = (event) => {
3187
+ if (!this.handle || event.target !== this.handle) return;
3188
+ this.write();
3189
+ this.onCommit();
3190
+ };
3191
+ /** Handle transform (canvas space) → clip child transform (group-relative). */
3192
+ write() {
3193
+ if (!this.handle || !this.child || !this.group) return;
3194
+ applyMatrix(
3195
+ this.child,
3196
+ import_fabric12.util.multiplyTransformMatrices(
3197
+ import_fabric12.util.invertTransform(matrixOf(this.group)),
3198
+ matrixOf(this.handle)
3199
+ )
3200
+ );
3201
+ this.group.dirty = true;
3202
+ this.group.set({ dirty: true });
3203
+ this.canvas.requestRenderAll();
3204
+ }
3205
+ };
3206
+
3207
+ // src/masks/store.ts
3208
+ var import_fabric15 = require("fabric");
3209
+
3210
+ // src/masks/host.ts
3211
+ var import_fabric13 = require("fabric");
3212
+ function findCanvasHost(layers) {
3213
+ return layers.getAll().find((layer) => layer.meta.canvasMask);
3214
+ }
3215
+ function createCanvasHost(canvas, layers) {
3216
+ const rect = new import_fabric13.Rect({
3217
+ left: 0,
3218
+ top: 0,
3219
+ width: canvas.getWidth(),
3220
+ height: canvas.getHeight(),
3221
+ originX: "left",
3222
+ originY: "top",
3223
+ fill: "#000000",
3224
+ globalCompositeOperation: "destination-in",
3225
+ // It is edited from the layer list, never on the canvas: a drag box on
3226
+ // something that cannot be dragged only reads as broken.
3227
+ selectable: false,
3228
+ evented: false,
3229
+ hasControls: false,
3230
+ hasBorders: false,
3231
+ objectCaching: false
3232
+ });
3233
+ const layer = layers.add("mask", rect, "Design mask");
3234
+ layer.meta.canvasMask = true;
3235
+ layers.setLocked(layer.id, true);
3236
+ return layer;
3237
+ }
3238
+ function pinCanvasHost(layers) {
3239
+ const all = layers.getAll();
3240
+ const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
3241
+ if (hostIndex === -1) return false;
3242
+ let lastContent = -1;
3243
+ all.forEach((layer, index) => {
3244
+ if (layer.type !== "mask") lastContent = index;
3245
+ });
3246
+ if (hostIndex >= lastContent) return false;
3247
+ layers.reorder(all[hostIndex].id, all.length - 1);
3248
+ return true;
3249
+ }
3250
+ function hostBoxOf(canvas, host, absolute) {
3251
+ if (!host) {
3252
+ return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
3253
+ }
3254
+ if (absolute) {
3255
+ host.setCoords();
3256
+ const rect = host.getBoundingRect();
3257
+ return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
3258
+ }
3259
+ const width = Math.max(1, host.width ?? 1);
3260
+ const height = Math.max(1, host.height ?? 1);
3261
+ return { left: -width / 2, top: -height / 2, width, height };
3262
+ }
3263
+
3264
+ // src/masks/install.ts
3265
+ var import_fabric14 = require("fabric");
3266
+ function convertSpace(host, sources, absolute) {
3267
+ const wasAbsolute = host.clipPath instanceof import_fabric14.Group ? host.clipPath.absolutePositioned : absolute;
3268
+ if (absolute === wasAbsolute) return;
3269
+ for (const source of sources) {
3270
+ if (absolute) toCanvasSpace(source, host);
3271
+ else toHostSpace(source, host);
3272
+ }
3273
+ }
3274
+ function withRelativeTransforms(entries, sources, host, absolute) {
3275
+ return entries.map((entry, index) => {
3276
+ const source = sources[index];
3277
+ if (absolute && entry.linked && source) {
3278
+ return { ...entry, rel: [...relativeMatrix(source, host)] };
3279
+ }
3280
+ if (!entry.rel) return entry;
3281
+ const rest = { ...entry };
3282
+ delete rest.rel;
3283
+ return rest;
3284
+ });
3285
+ }
3286
+ function installClip(host, entries, sources, box, absolute) {
3287
+ host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
3288
+ host.dirty = true;
3289
+ host.setCoords();
3290
+ }
3291
+
3292
+ // src/masks/store.ts
3293
+ var CANVAS_MASK_TARGET = "canvas";
3294
+ var DEFAULT_ENTRY = {
3295
+ mode: "add",
3296
+ linked: true,
3297
+ visible: true,
3298
+ opacity: 1
3299
+ };
3300
+ var MaskStackStore = class {
3301
+ constructor(canvas, layers, history, events) {
3302
+ this.canvas = canvas;
3303
+ this.layers = layers;
3304
+ this.history = history;
3305
+ this.events = events;
3306
+ }
3307
+ canvas;
3308
+ layers;
3309
+ history;
3310
+ events;
3311
+ selected = null;
3312
+ pinning = false;
3313
+ list(target) {
3314
+ return this.hostLayer(target)?.meta.maskStack ?? [];
3315
+ }
3316
+ get(target, maskId) {
3317
+ return this.list(target).find((entry) => entry.id === maskId);
3318
+ }
3319
+ /** Every target that currently carries at least one mask. */
3320
+ targets() {
3321
+ return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
3322
+ }
3323
+ /** The box a mask is fitted to, in the space the stack is composed in. */
3324
+ hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
3325
+ return hostBoxOf(this.canvas, this.host(target), absolute);
3326
+ }
3327
+ /** The host layer of a target, optionally creating the design overlay. */
3328
+ hostLayer(target, create = false) {
3329
+ if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
3330
+ const existing = findCanvasHost(this.layers);
3331
+ if (existing || !create) return existing;
3332
+ return createCanvasHost(this.canvas, this.layers);
3333
+ }
3334
+ host(target, create = false) {
3335
+ return this.hostLayer(target, create)?.fabricObject ?? null;
3336
+ }
3337
+ /** Re-pin the design overlay, guarding the reorder that re-triggers this. */
3338
+ pin() {
3339
+ if (this.pinning) return;
3340
+ this.pinning = true;
3341
+ try {
3342
+ pinCanvasHost(this.layers);
3343
+ } finally {
3344
+ this.pinning = false;
3345
+ }
3346
+ }
3347
+ /**
3348
+ * Take the current geometry back out of the composed clip, entry-aligned.
3349
+ *
3350
+ * The entry list is the authority on how to read the clip: with no entries the
3351
+ * clip predates the stack (a mask preset, or a single `clipPath` an older host
3352
+ * installed) and is one mask whole — including when it happens to be a group,
3353
+ * which is why this cannot just unwrap anything group-shaped.
3354
+ */
3355
+ unwrap(target, host) {
3356
+ const clip = host.clipPath;
3357
+ if (!clip) return [];
3358
+ const entries = this.list(target);
3359
+ if (entries.length === 0 || !(clip instanceof import_fabric15.Group)) return [asObject(clip)];
3360
+ const children = unwrapGroup(clip);
3361
+ const extra = children.length - entries.length;
3362
+ return extra > 0 ? children.slice(extra) : children;
3363
+ }
3364
+ /**
3365
+ * Entries for a stack, adopting a pre-stack clip as the first one. Without
3366
+ * this, the first `add()` on an already-masked layer would compose a clip it
3367
+ * has no entry for and silently throw that mask away.
3368
+ */
3369
+ entriesFor(target, sources) {
3370
+ const existing = this.list(target);
3371
+ if (existing.length > 0 || sources.length !== 1) return [...existing];
3372
+ const layer = this.hostLayer(target);
3373
+ const preset = layer?.meta.maskPreset;
3374
+ if (layer) {
3375
+ delete layer.meta.maskPreset;
3376
+ }
3377
+ return [
3378
+ {
3379
+ ...DEFAULT_ENTRY,
3380
+ id: generateId(),
3381
+ name: typeof preset === "string" ? preset : "Mask",
3382
+ linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
3383
+ }
3384
+ ];
3385
+ }
3386
+ /**
3387
+ * Install a stack: convert geometry into the space the stack needs, compose the
3388
+ * clip, store the entries, and commit one history checkpoint for the lot.
3389
+ */
3390
+ commit(target, host, entries, sources, save = true, forceAbsolute = false) {
3391
+ const layer = this.hostLayer(target);
3392
+ if (!layer) return;
3393
+ const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
3394
+ convertSpace(host, sources, absolute);
3395
+ const next = withRelativeTransforms(entries, sources, host, absolute);
3396
+ installClip(host, next, sources, this.hostBox(target, absolute), absolute);
3397
+ if (next.length === 0) {
3398
+ delete layer.meta.maskStack;
3399
+ if (layer.meta.canvasMask) this.layers.remove(layer.id);
3400
+ } else {
3401
+ layer.meta.maskStack = next;
3402
+ }
3403
+ this.canvas.requestRenderAll();
3404
+ this.events.emit("masks:changed", { target });
3405
+ this.events.emit("layer:modified", { layerId: layer.id });
3406
+ if (save) this.history.save();
3407
+ }
3408
+ };
3409
+
3410
+ // src/masks/manager.ts
3411
+ var LayerMaskManager = class extends MaskStackStore {
3412
+ // Committing mid-drag would recompose the group the handle writes into and
3413
+ // leave it pointing at a discarded object; the geometry is settled on endEdit.
3414
+ edits = new MaskEditController(this.canvas, () => this.history.save());
3415
+ onLayersChanged = () => this.pin();
3416
+ // Selecting a layer means the user has moved on from the mask they had open;
3417
+ // leaving both selected would show mask controls for an unrelated layer.
3418
+ onLayerSelected = () => {
3419
+ if (this.selected) this.select(null, null);
3420
+ };
3421
+ onObjectModified = (event) => {
3422
+ const object = event.target;
3423
+ if (!object) return;
3424
+ const layer = this.layers.findByObject(object);
3425
+ if (layer) this.reflow(layer.id);
3426
+ };
3427
+ constructor(canvas, layers, history, events) {
3428
+ super(canvas, layers, history, events);
3429
+ this.events.on("layers:changed", this.onLayersChanged);
3430
+ this.events.on("layer:selected", this.onLayerSelected);
3431
+ this.canvas.on("object:modified", this.onObjectModified);
3432
+ }
3433
+ dispose() {
3434
+ this.edits.dispose();
3435
+ this.events.off("layers:changed", this.onLayersChanged);
3436
+ this.events.off("layer:selected", this.onLayerSelected);
3437
+ this.canvas.off("object:modified", this.onObjectModified);
3438
+ this.selected = null;
3439
+ }
3440
+ // ─── Geometry editing ────────────────────────────────
3441
+ /** The mask currently being dragged on the canvas, if any. */
3442
+ editing() {
3443
+ return this.edits.active();
3444
+ }
3445
+ /**
3446
+ * Put drag handles on one mask. The stack is composed in canvas space for the
3447
+ * duration — a linked mask would otherwise sit in the host's space, where the
3448
+ * handle's own canvas coordinates mean something else entirely. `endEdit`
3449
+ * returns it to whichever space its entries call for.
3450
+ */
3451
+ async beginEdit(target, maskId) {
3452
+ const host = this.host(target);
3453
+ if (!host) return false;
3454
+ const entries = this.list(target);
3455
+ const index = entries.findIndex((entry) => entry.id === maskId);
3456
+ if (index === -1) return false;
3457
+ this.endEdit(false);
3458
+ this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
3459
+ const group = host.clipPath instanceof import_fabric16.Group ? host.clipPath : null;
3460
+ if (!group) return false;
3461
+ const children = group.getObjects();
3462
+ const child = children[children.length - entries.length + index];
3463
+ if (!child) return false;
3464
+ this.select(target, maskId);
3465
+ return this.edits.begin({ target, maskId }, group, child);
3466
+ }
3467
+ /** Take the handles down and settle the stack back into its own space. */
3468
+ endEdit(save = true) {
3469
+ const editing = this.edits.active();
3470
+ this.edits.end();
3471
+ if (editing) this.rebuild(editing.target, save);
3472
+ }
3473
+ // ─── Selection ───────────────────────────────────────
3474
+ getSelected() {
3475
+ return this.selected;
3476
+ }
3477
+ select(target, maskId) {
3478
+ this.selected = target && maskId ? { target, maskId } : null;
3479
+ this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
3480
+ }
3481
+ // ─── Mutations ───────────────────────────────────────
3482
+ add(target, object, options = {}) {
3483
+ const host = this.host(target, true);
3484
+ if (!host) return null;
3485
+ const sources = this.unwrap(target, host);
3486
+ const entries = this.entriesFor(target, sources);
3487
+ const entry = {
3488
+ ...DEFAULT_ENTRY,
3489
+ id: generateId(),
3490
+ name: options.name ?? "Mask",
3491
+ ...options.mode ? { mode: options.mode } : {},
3492
+ ...options.linked === void 0 ? {} : { linked: options.linked },
3493
+ ...options.meta ? { meta: options.meta } : {}
3494
+ };
3495
+ if (target === CANVAS_MASK_TARGET) entry.linked = false;
3496
+ if (options.fit !== false) {
3497
+ fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
3498
+ }
3499
+ sources.push(object);
3500
+ entries.push(entry);
3501
+ this.commit(target, host, entries, sources);
3502
+ return entry;
3503
+ }
3504
+ /** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
3505
+ replaceGeometry(target, maskId, object, options = {}) {
3506
+ const host = this.host(target);
3507
+ if (!host) return false;
3508
+ const entries = [...this.list(target)];
3509
+ const index = entries.findIndex((entry) => entry.id === maskId);
3510
+ if (index === -1) return false;
3511
+ const sources = this.unwrap(target, host);
3512
+ if (options.fit !== false) {
3513
+ fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
3514
+ }
3515
+ sources[index] = object;
3516
+ entries[index] = {
3517
+ ...entries[index],
3518
+ ...options.name ? { name: options.name } : {},
3519
+ ...options.meta ? { meta: options.meta } : {}
3520
+ };
3521
+ this.commit(target, host, entries, sources);
3522
+ return true;
3523
+ }
3524
+ /**
3525
+ * Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
3526
+ * geometry it already has. This is what a zoom control drives: re-picking the
3527
+ * mask would cost another render of a generated alpha map just to resize it.
3528
+ */
3529
+ refit(target, maskId, options = {}) {
3530
+ const host = this.host(target);
3531
+ if (!host) return false;
3532
+ const entries = [...this.list(target)];
3533
+ const index = entries.findIndex((entry) => entry.id === maskId);
3534
+ if (index === -1) return false;
3535
+ const sources = this.unwrap(target, host);
3536
+ const source = sources[index];
3537
+ if (!source) return false;
3538
+ fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
3539
+ this.commit(target, host, entries, sources, options.save ?? true);
3540
+ return true;
3541
+ }
3542
+ remove(target, maskId) {
3543
+ const host = this.host(target);
3544
+ if (!host) return false;
3545
+ const entries = [...this.list(target)];
3546
+ const index = entries.findIndex((entry) => entry.id === maskId);
3547
+ if (index === -1) return false;
3548
+ const sources = this.unwrap(target, host);
3549
+ entries.splice(index, 1);
3550
+ sources.splice(index, 1);
3551
+ if (this.selected?.maskId === maskId) this.select(null, null);
3552
+ this.commit(target, host, entries, sources);
3553
+ return true;
3554
+ }
3555
+ clear(target) {
3556
+ const host = this.host(target);
3557
+ if (!host) return false;
3558
+ if (this.list(target).length === 0) return false;
3559
+ if (this.selected?.target === target) this.select(null, null);
3560
+ this.commit(target, host, [], []);
3561
+ return true;
3562
+ }
3563
+ reorder(target, maskId, index) {
3564
+ const host = this.host(target);
3565
+ if (!host) return false;
3566
+ const entries = [...this.list(target)];
3567
+ const from = entries.findIndex((entry2) => entry2.id === maskId);
3568
+ if (from === -1) return false;
3569
+ const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
3570
+ if (from === to) return false;
3571
+ const sources = this.unwrap(target, host);
3572
+ const [entry] = entries.splice(from, 1);
3573
+ const [source] = sources.splice(from, 1);
3574
+ entries.splice(to, 0, entry);
3575
+ sources.splice(to, 0, source);
3576
+ this.commit(target, host, entries, sources);
3577
+ return true;
3578
+ }
3579
+ /** Replace a mask's host metadata (which preset or generator produced it). */
3580
+ setMeta(target, maskId, meta) {
3581
+ return this.patch(target, maskId, () => ({ meta }));
3582
+ }
3583
+ setMode(target, maskId, mode) {
3584
+ return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
3585
+ }
3586
+ setVisible(target, maskId, visible) {
3587
+ return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
3588
+ }
3589
+ setOpacity(target, maskId, opacity) {
3590
+ if (!Number.isFinite(opacity)) return false;
3591
+ const next = Math.max(0, Math.min(1, opacity));
3592
+ return this.patch(
3593
+ target,
3594
+ maskId,
3595
+ (entry) => entry.opacity === next ? null : { opacity: next }
3596
+ );
3597
+ }
3598
+ setName(target, maskId, name) {
3599
+ const trimmed = name.trim();
3600
+ if (!trimmed) return false;
3601
+ return this.patch(
3602
+ target,
3603
+ maskId,
3604
+ (entry) => entry.name === trimmed ? null : { name: trimmed }
3605
+ );
3606
+ }
3607
+ /**
3608
+ * Link or unlink one mask. Unlinking pins it where it currently appears;
3609
+ * re-linking records its position relative to the host so later host moves
3610
+ * carry it along.
3611
+ */
3612
+ setLinked(target, maskId, linked) {
3613
+ if (target === CANVAS_MASK_TARGET) return false;
3614
+ const host = this.host(target);
3615
+ if (!host) return false;
3616
+ const entries = [...this.list(target)];
3617
+ const index = entries.findIndex((entry) => entry.id === maskId);
3618
+ if (index === -1 || entries[index].linked === linked) return false;
3619
+ const sources = this.unwrap(target, host);
3620
+ entries[index] = { ...entries[index], linked };
3621
+ this.commit(target, host, entries, sources);
3622
+ return true;
3623
+ }
3624
+ /**
3625
+ * Consume a layer, turning its artwork into a mask. Without an explicit target
3626
+ * it masks the layer directly beneath it, and the bottom layer masks the whole
3627
+ * design — there is nothing under it to clip.
3628
+ */
3629
+ convertLayer(layerId, target) {
3630
+ const layer = this.layers.get(layerId);
3631
+ if (!layer || layer.meta.canvasMask) return null;
3632
+ const ordered = this.layers.getAll();
3633
+ const index = ordered.findIndex((candidate) => candidate.id === layerId);
3634
+ const below = index > 0 ? ordered[index - 1] : void 0;
3635
+ const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
3636
+ if (resolved === layerId) return null;
3637
+ const object = layer.fabricObject;
3638
+ if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
3639
+ this.history.beginTransaction();
3640
+ try {
3641
+ if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
3642
+ this.canvas.remove(object);
3643
+ this.layers.remove(layerId);
3644
+ const entry = this.add(resolved, object, { name: layer.name, fit: false });
3645
+ return entry ? { target: resolved, entry } : null;
3646
+ } finally {
3647
+ this.history.endTransaction();
3648
+ }
3649
+ }
3650
+ // ─── Rebuild / restore ───────────────────────────────
3651
+ /** Recompose one stack's clip from the geometry it already holds. */
3652
+ rebuild(target, save = false) {
3653
+ const host = this.host(target);
3654
+ if (!host) return;
3655
+ const sources = this.unwrap(target, host);
3656
+ if (sources.length === 0) return;
3657
+ this.commit(target, host, this.entriesFor(target, sources), sources, save);
3658
+ }
3659
+ /** Re-derive linked masks after the host moved (canvas-space stacks only). */
3660
+ reflow(target) {
3661
+ const host = this.host(target);
3662
+ if (!host) return;
3663
+ const entries = this.list(target);
3664
+ if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
3665
+ if (!entries.some((entry) => entry.linked && entry.rel)) return;
3666
+ const sources = this.unwrap(target, host);
3667
+ entries.forEach((entry, index) => {
3668
+ const source = sources[index];
3669
+ const rel = toMatrix(entry.rel);
3670
+ if (!source || !entry.linked || !rel) return;
3671
+ applyRelativeMatrix(source, host, rel);
3672
+ });
3673
+ this.commit(target, host, [...entries], sources);
3674
+ }
3675
+ /**
3676
+ * Adopt a clip this manager did not install — a host's own single `clipPath` —
3677
+ * as a one-entry stack, so it shows up in the UI as a mask row before anything
3678
+ * is added to it. A mask preset is left alone unless asked for by name: it is
3679
+ * still owned by `MaskPresetManager` until a stack operation takes it over.
3680
+ */
3681
+ adopt(target, name) {
3682
+ const layer = this.hostLayer(target);
3683
+ const clip = layer?.fabricObject.clipPath;
3684
+ if (!layer || !clip || layer.meta.pattern) return null;
3685
+ if ((layer.meta.maskStack ?? []).length > 0) return null;
3686
+ const entry = {
3687
+ ...DEFAULT_ENTRY,
3688
+ id: generateId(),
3689
+ name: name ?? "Mask",
3690
+ linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
3691
+ };
3692
+ layer.meta.maskStack = [entry];
3693
+ delete layer.meta.maskPreset;
3694
+ this.rebuild(target);
3695
+ return entry;
3696
+ }
3697
+ /** After a state restore: re-pin the design overlay and recompose every stack. */
3698
+ refreshAll() {
3699
+ this.selected = null;
3700
+ this.pin();
3701
+ for (const layer of this.layers.getAll()) {
3702
+ if ((layer.meta.maskStack ?? []).length === 0) continue;
3703
+ this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
3704
+ }
3705
+ }
3706
+ // ─── Internals ───────────────────────────────────────
3707
+ patch(target, maskId, change) {
3708
+ const host = this.host(target);
3709
+ if (!host) return false;
3710
+ const entries = [...this.list(target)];
3711
+ const index = entries.findIndex((entry) => entry.id === maskId);
3712
+ if (index === -1) return false;
3713
+ const patch = change(entries[index]);
3714
+ if (!patch) return false;
3715
+ const sources = this.unwrap(target, host);
3716
+ entries[index] = { ...entries[index], ...patch };
3717
+ this.commit(target, host, entries, sources);
3718
+ return true;
3719
+ }
3720
+ };
3721
+
2715
3722
  // src/editor.ts
2716
3723
  var MIN_ZOOM = 0.1;
2717
3724
  var MAX_ZOOM = 8;
@@ -2729,6 +3736,8 @@ var CanvasEditor = class {
2729
3736
  patterns;
2730
3737
  curves;
2731
3738
  maskPresets;
3739
+ /** Stacked boolean masks, per layer and for the design as a whole. */
3740
+ layerMasks;
2732
3741
  fonts;
2733
3742
  licensing;
2734
3743
  pages;
@@ -2753,7 +3762,7 @@ var CanvasEditor = class {
2753
3762
  const widthPx = this.units.toPixels(config.width);
2754
3763
  const heightPx = this.units.toPixels(config.height);
2755
3764
  this.designBackground = config.backgroundColor ?? "#ffffff";
2756
- this.canvas = new import_fabric9.Canvas(canvasElement, {
3765
+ this.canvas = new import_fabric17.Canvas(canvasElement, {
2757
3766
  width: widthPx,
2758
3767
  height: heightPx,
2759
3768
  backgroundColor: this.designBackground,
@@ -2774,15 +3783,10 @@ var CanvasEditor = class {
2774
3783
  this.layers.setHistoryCallback(() => this.history.save());
2775
3784
  this.snapping = new SnapManager(this.canvas, this.events);
2776
3785
  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
- );
3786
+ this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
2784
3787
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
2785
3788
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
3789
+ this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
2786
3790
  this.setupCanvasEvents();
2787
3791
  this.refreshSelectionStyle();
2788
3792
  this.history.saveImmediate();
@@ -2792,12 +3796,13 @@ var CanvasEditor = class {
2792
3796
  // ─── Layer Operations ────────────────────────────────
2793
3797
  async addImage(url, options) {
2794
3798
  try {
2795
- const img = await import_fabric9.FabricImage.fromURL(
3799
+ const img = await import_fabric17.FabricImage.fromURL(
2796
3800
  url,
2797
3801
  {},
2798
3802
  { originX: "left", originY: "top", ...options }
2799
3803
  );
2800
3804
  const layer = this.layers.add("image", img);
3805
+ this.layers.select(layer.id);
2801
3806
  this.history.save();
2802
3807
  return layer;
2803
3808
  } catch (error) {
@@ -2817,7 +3822,7 @@ var CanvasEditor = class {
2817
3822
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
2818
3823
  const previous = layer.fabricObject;
2819
3824
  try {
2820
- const replacement = await import_fabric9.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
3825
+ const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2821
3826
  replacement.set({
2822
3827
  left: previous.left,
2823
3828
  top: previous.top,
@@ -2846,7 +3851,7 @@ var CanvasEditor = class {
2846
3851
  }
2847
3852
  }
2848
3853
  addText(text, options) {
2849
- const textbox = new import_fabric9.Textbox(text, {
3854
+ const textbox = new import_fabric17.Textbox(text, {
2850
3855
  fontSize: 32,
2851
3856
  fontFamily: "Arial",
2852
3857
  fill: "#000000",
@@ -2857,12 +3862,14 @@ var CanvasEditor = class {
2857
3862
  ...options
2858
3863
  });
2859
3864
  const layer = this.layers.add("text", textbox);
3865
+ this.layers.select(layer.id);
2860
3866
  this.history.save();
2861
3867
  return layer;
2862
3868
  }
2863
3869
  addShape(plugin, options) {
2864
3870
  const obj = plugin.create(options);
2865
3871
  const layer = this.layers.add("shape", obj, plugin.name);
3872
+ this.layers.select(layer.id);
2866
3873
  this.history.save();
2867
3874
  return layer;
2868
3875
  }
@@ -2885,10 +3892,10 @@ var CanvasEditor = class {
2885
3892
  return value === void 0 ? token : escapeXml(value);
2886
3893
  })
2887
3894
  );
2888
- const { objects, options } = await (0, import_fabric9.loadSVGFromString)(resolved);
3895
+ const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
2889
3896
  const validObjects = objects.filter((object) => object !== null);
2890
3897
  if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
2891
- const group = import_fabric9.util.groupSVGElements(validObjects, options);
3898
+ const group = import_fabric17.util.groupSVGElements(validObjects, options);
2892
3899
  group.set({
2893
3900
  left: this.canvas.getWidth() / 2,
2894
3901
  top: this.canvas.getHeight() / 2,
@@ -2896,6 +3903,7 @@ var CanvasEditor = class {
2896
3903
  originY: "center"
2897
3904
  });
2898
3905
  const layer = this.layers.add("template", group, template.name);
3906
+ this.layers.select(layer.id);
2899
3907
  this.history.save();
2900
3908
  return layer;
2901
3909
  }
@@ -2933,9 +3941,7 @@ var CanvasEditor = class {
2933
3941
  const layer = this.layers.get(id);
2934
3942
  if (!layer) return null;
2935
3943
  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
- }
3944
+ clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
2939
3945
  clone.setCoords();
2940
3946
  const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
2941
3947
  copy.meta = structuredClone(layer.meta);
@@ -2948,7 +3954,8 @@ var CanvasEditor = class {
2948
3954
  evented: !layer.locked,
2949
3955
  opacity: layer.opacity
2950
3956
  });
2951
- this.canvas.setActiveObject(clone);
3957
+ this.patterns.attachTo(copy);
3958
+ this.layers.select(copy.id);
2952
3959
  this.canvas.requestRenderAll();
2953
3960
  this.history.save();
2954
3961
  return copy;
@@ -2961,10 +3968,10 @@ var CanvasEditor = class {
2961
3968
  const next = { ...previous, ...adjustments };
2962
3969
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
2963
3970
  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) })
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) })
2968
3975
  ];
2969
3976
  layer.meta.imageAdjustments = next;
2970
3977
  image.applyFilters();
@@ -2981,7 +3988,7 @@ var CanvasEditor = class {
2981
3988
  const childData = children.map((layer) => structuredClone(layer.toData()));
2982
3989
  const objects = children.map((layer) => layer.fabricObject);
2983
3990
  for (const layer of children) this.layers.remove(layer.id);
2984
- const group = new import_fabric9.Group(objects);
3991
+ const group = new import_fabric17.Group(objects);
2985
3992
  const grouped = this.layers.add("group", group, name);
2986
3993
  grouped.meta.groupChildren = childData;
2987
3994
  this.layers.select(grouped.id);
@@ -2996,11 +4003,9 @@ var CanvasEditor = class {
2996
4003
  if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
2997
4004
  const group = grouped.fabricObject;
2998
4005
  return this.history.transaction(() => {
2999
- const transform = group.calcTransformMatrix();
3000
4006
  const objects = group.removeAll();
3001
4007
  this.layers.remove(id);
3002
4008
  const restored = objects.map((object, index) => {
3003
- import_fabric9.util.addTransformToObject(object, transform);
3004
4009
  object.setCoords();
3005
4010
  const data = childData[index];
3006
4011
  const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
@@ -3027,7 +4032,8 @@ var CanvasEditor = class {
3027
4032
  }
3028
4033
  try {
3029
4034
  await deserializeEditor(this, state);
3030
- this.patterns.repinAll();
4035
+ await this.patterns.rehydrateAll();
4036
+ this.layerMasks.refreshAll();
3031
4037
  } catch (error) {
3032
4038
  if (!managedByHistory) {
3033
4039
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -3056,7 +4062,7 @@ var CanvasEditor = class {
3056
4062
  const layer = this.layers.get(id);
3057
4063
  if (!layer) throw new Error(`Layer not found: ${id}`);
3058
4064
  try {
3059
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric9.FabricImage) {
4065
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
3060
4066
  const image = await layer.fabricObject.clone();
3061
4067
  image.set({
3062
4068
  left: 0,
@@ -3076,7 +4082,11 @@ var CanvasEditor = class {
3076
4082
  cloneObjects: false
3077
4083
  });
3078
4084
  }
3079
- return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
4085
+ return await exportIsolatedPNG(
4086
+ this.canvas,
4087
+ [layer.renderProxy ?? layer.fabricObject],
4088
+ options
4089
+ );
3080
4090
  } catch (error) {
3081
4091
  this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
3082
4092
  throw error;
@@ -3328,7 +4338,7 @@ var CanvasEditor = class {
3328
4338
  return;
3329
4339
  }
3330
4340
  try {
3331
- const image = await import_fabric9.FabricImage.fromURL(
4341
+ const image = await import_fabric17.FabricImage.fromURL(
3332
4342
  url,
3333
4343
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
3334
4344
  { originX: "left", originY: "top" }
@@ -3381,7 +4391,6 @@ var CanvasEditor = class {
3381
4391
  const sx = widthPx / oldWidth;
3382
4392
  const sy = heightPx / oldHeight;
3383
4393
  for (const layer of this.layers.getAll()) {
3384
- if (layer.meta.pattern) continue;
3385
4394
  const object = layer.fabricObject;
3386
4395
  object.set({
3387
4396
  left: (object.left ?? 0) * sx,
@@ -3402,7 +4411,7 @@ var CanvasEditor = class {
3402
4411
  }
3403
4412
  }
3404
4413
  this.canvas.setDimensions({ width: widthPx, height: heightPx });
3405
- this.patterns.repinAll();
4414
+ this.patterns.syncArea();
3406
4415
  this.canvas.requestRenderAll();
3407
4416
  this.history.save();
3408
4417
  this.events.emit("canvas:modified", {});
@@ -3597,10 +4606,11 @@ var CanvasEditor = class {
3597
4606
  // ─── Cleanup ────────────────────────────────────────
3598
4607
  dispose() {
3599
4608
  this.masks.dispose();
4609
+ this.layerMasks.dispose();
3600
4610
  this.snapping.dispose();
3601
4611
  this.crop.dispose();
3602
4612
  this.history.dispose();
3603
- clearPatternImageCache();
4613
+ this.patterns.dispose();
3604
4614
  this.events.removeAllListeners();
3605
4615
  this.canvas.dispose();
3606
4616
  }
@@ -3720,6 +4730,7 @@ var AnnotationOverlay = class {
3720
4730
  // Annotate the CommonJS export names for ESM import in node:
3721
4731
  0 && (module.exports = {
3722
4732
  AnnotationOverlay,
4733
+ CANVAS_MASK_TARGET,
3723
4734
  CANVAS_SIZE_PRESETS,
3724
4735
  CanvasEditor,
3725
4736
  CropController,
@@ -3732,6 +4743,7 @@ var AnnotationOverlay = class {
3732
4743
  HistoryManager,
3733
4744
  Layer,
3734
4745
  LayerManager,
4746
+ LayerMaskManager,
3735
4747
  LicenseManager,
3736
4748
  MaskController,
3737
4749
  MaskPresetManager,
@@ -3744,18 +4756,16 @@ var AnnotationOverlay = class {
3744
4756
  TEXTURE_MASK_IDS,
3745
4757
  TEXTURE_MASK_SIZE,
3746
4758
  TextCurveManager,
4759
+ TiledPatternObject,
3747
4760
  UnitConverter,
3748
4761
  applyAspectLock,
3749
4762
  applyLayerShadow,
3750
4763
  applyObjectSelectionStyle,
3751
- applyPatternLocks,
3752
4764
  applySelectionStyle,
3753
4765
  buildCurvePathData,
3754
- buildPatternDataURL,
3755
- captureLocks,
3756
4766
  clamp,
3757
- clearPatternImageCache,
3758
4767
  clearTextureMaskCache,
4768
+ composeMaskGroup,
3759
4769
  computeCoverPlacement,
3760
4770
  computePrintAreaClip,
3761
4771
  computeTilePositions,
@@ -3768,12 +4778,13 @@ var AnnotationOverlay = class {
3768
4778
  exportPNG,
3769
4779
  exportPrintArea,
3770
4780
  exportSVG,
4781
+ fitToBox,
3771
4782
  generateId,
3772
4783
  isCssColor,
3773
4784
  isMaskPresetId,
3774
4785
  isShapeMaskId,
3775
4786
  isTextureMaskId,
3776
- loadPatternImage,
4787
+ needsAbsoluteSpace,
3777
4788
  readLayerShadow,
3778
4789
  renderTextureMask,
3779
4790
  resetTransform,
@@ -3781,6 +4792,9 @@ var AnnotationOverlay = class {
3781
4792
  round2,
3782
4793
  sanitizeSvg,
3783
4794
  serializeEditor,
3784
- shapeMaskPathData
4795
+ shapeMaskPathData,
4796
+ toCanvasSpace,
4797
+ toHostSpace,
4798
+ unwrapGroup
3785
4799
  });
3786
4800
  //# sourceMappingURL=index.js.map