@overtone-art/canvas-editor-core 0.4.0 → 0.5.1

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,
@@ -54,6 +56,7 @@ __export(index_exports, {
54
56
  buildCurvePathData: () => buildCurvePathData,
55
57
  clamp: () => clamp,
56
58
  clearTextureMaskCache: () => clearTextureMaskCache,
59
+ composeMaskGroup: () => composeMaskGroup,
57
60
  computeCoverPlacement: () => computeCoverPlacement,
58
61
  computePrintAreaClip: () => computePrintAreaClip,
59
62
  computeTilePositions: () => computeTilePositions,
@@ -66,11 +69,13 @@ __export(index_exports, {
66
69
  exportPNG: () => exportPNG,
67
70
  exportPrintArea: () => exportPrintArea,
68
71
  exportSVG: () => exportSVG,
72
+ fitToBox: () => fitToBox,
69
73
  generateId: () => generateId,
70
74
  isCssColor: () => isCssColor,
71
75
  isMaskPresetId: () => isMaskPresetId,
72
76
  isShapeMaskId: () => isShapeMaskId,
73
77
  isTextureMaskId: () => isTextureMaskId,
78
+ needsAbsoluteSpace: () => needsAbsoluteSpace,
74
79
  readLayerShadow: () => readLayerShadow,
75
80
  renderTextureMask: () => renderTextureMask,
76
81
  resetTransform: () => resetTransform,
@@ -78,12 +83,15 @@ __export(index_exports, {
78
83
  round2: () => round2,
79
84
  sanitizeSvg: () => sanitizeSvg,
80
85
  serializeEditor: () => serializeEditor,
81
- shapeMaskPathData: () => shapeMaskPathData
86
+ shapeMaskPathData: () => shapeMaskPathData,
87
+ toCanvasSpace: () => toCanvasSpace,
88
+ toHostSpace: () => toHostSpace,
89
+ unwrapGroup: () => unwrapGroup
82
90
  });
83
91
  module.exports = __toCommonJS(index_exports);
84
92
 
85
93
  // src/editor.ts
86
- var import_fabric10 = require("fabric");
94
+ var import_fabric17 = require("fabric");
87
95
 
88
96
  // src/events.ts
89
97
  var EventEmitter = class {
@@ -245,7 +253,13 @@ var LayerManager = class {
245
253
  if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
246
254
  layer.renderProxy = proxy;
247
255
  if (proxy) {
248
- proxy.set({ visible: layer.visible, opacity: layer.opacity });
256
+ proxy._layerId = id;
257
+ proxy.set({
258
+ visible: layer.visible,
259
+ opacity: layer.opacity,
260
+ selectable: !layer.locked,
261
+ evented: !layer.locked
262
+ });
249
263
  this.canvas.add(proxy);
250
264
  this.syncZOrder();
251
265
  }
@@ -284,7 +298,7 @@ var LayerManager = class {
284
298
  } else {
285
299
  const layer = this.get(id);
286
300
  if (layer) {
287
- this.canvas.setActiveObject(layer.fabricObject);
301
+ this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
288
302
  }
289
303
  }
290
304
  this.canvas.requestRenderAll();
@@ -319,8 +333,9 @@ var LayerManager = class {
319
333
  if (!layer) return;
320
334
  if (layer.locked === locked) return;
321
335
  layer.locked = locked;
322
- layer.fabricObject.selectable = !locked;
323
- layer.fabricObject.evented = !locked;
336
+ const target = layer.renderProxy ?? layer.fabricObject;
337
+ target.selectable = !locked;
338
+ target.evented = !locked;
324
339
  this.canvas.requestRenderAll();
325
340
  this.emitChanged();
326
341
  this.onPropertyChanged?.();
@@ -951,8 +966,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
951
966
  }
952
967
  return placements;
953
968
  }
954
- function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
969
+ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
955
970
  const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
971
+ const drawW = draw && draw.width > 0 ? draw.width : baseW;
972
+ const drawH = draw && draw.height > 0 ? draw.height : baseH;
956
973
  ctx.save();
957
974
  ctx.translate(anchor.x, anchor.y);
958
975
  ctx.rotate(config.angle * Math.PI / 180);
@@ -960,7 +977,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
960
977
  ctx.save();
961
978
  ctx.translate(tile.x, tile.y);
962
979
  ctx.rotate(tile.rotation);
963
- ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
980
+ ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
964
981
  ctx.restore();
965
982
  }
966
983
  ctx.restore();
@@ -982,46 +999,43 @@ function mod2(n) {
982
999
  var MAX_SNAPSHOT_PIXELS = 16e6;
983
1000
  var MAX_SNAPSHOT_SCALE = 8;
984
1001
  var SNAPSHOT_SHRINK_FACTOR = 2;
1002
+ var TILE_BLEED_DEVICE_PX = 2;
985
1003
  var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
986
1004
  static type = "TiledPattern";
987
1005
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
988
1006
  source;
989
1007
  config;
1008
+ /** The print area this pattern fills, in canvas units. */
1009
+ area;
990
1010
  snapshotEl = null;
991
1011
  snapshotScale = 0;
992
- constructor(source, config, width, height) {
1012
+ constructor(source, config, area) {
993
1013
  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,
1014
+ // Centre origin: the box tracks a tile, and a tile is placed by its centre.
1015
+ originX: "center",
1016
+ originY: "center",
1007
1017
  objectCaching: false,
1008
- hasControls: false,
1009
- hasBorders: false
1018
+ // Side handles would imply a non-uniform tile scale; the config has one.
1019
+ lockScalingFlip: true
1010
1020
  });
1011
1021
  this.source = source;
1012
1022
  this.config = config;
1023
+ this.area = area;
1024
+ this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
1025
+ this.syncBox();
1013
1026
  }
1014
- /** Swap in a new config; the next render picks it up. */
1027
+ /** Swap in a new config and re-fit the box to the tile it now describes. */
1015
1028
  setConfig(config) {
1016
1029
  this.config = config;
1017
- this.dirty = true;
1030
+ this.syncBox();
1018
1031
  }
1019
- /** Resize to a new print area. */
1020
- setArea(width, height) {
1021
- this.set({ width, height });
1022
- this.setCoords();
1032
+ /** Re-fit to a new print area (canvas resize). */
1033
+ setArea(area) {
1034
+ this.area = area;
1035
+ this.invalidate();
1036
+ this.syncBox();
1023
1037
  }
1024
- /** Drop the cached source snapshot (e.g. the source was edited). */
1038
+ /** Drop the cached source snapshot (the source was edited). */
1025
1039
  invalidate() {
1026
1040
  this.snapshotEl = null;
1027
1041
  this.snapshotScale = 0;
@@ -1032,40 +1046,118 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1032
1046
  this.snapshotEl = null;
1033
1047
  this.snapshotScale = 0;
1034
1048
  }
1049
+ /**
1050
+ * Put the box back on the anchor tile: the tile's size, the pattern angle, and
1051
+ * the grid origin displaced by the configured phase shift. Resets any live
1052
+ * gesture scale, so it must run only once that gesture has been folded in.
1053
+ */
1054
+ syncBox() {
1055
+ const { tileW, tileH } = this.baseTile();
1056
+ const origin = this.source.getCenterPoint();
1057
+ const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
1058
+ this.set({
1059
+ left: anchor.x,
1060
+ top: anchor.y,
1061
+ width: tileW,
1062
+ height: tileH,
1063
+ scaleX: 1,
1064
+ scaleY: 1,
1065
+ angle: this.config.angle
1066
+ });
1067
+ this.setCoords();
1068
+ this.dirty = true;
1069
+ }
1070
+ /** Grid rotation in play right now — the live handle during a rotate gesture. */
1071
+ liveAngle() {
1072
+ return this.angle ?? 0;
1073
+ }
1074
+ /** Tile scale in play right now, as a config percentage. */
1075
+ liveScale() {
1076
+ return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
1077
+ }
1078
+ /**
1079
+ * Where the source's centre would have to sit for the box to stay put — i.e.
1080
+ * the grid origin implied by a drag. `PatternManager` moves the source there.
1081
+ */
1082
+ liveOrigin() {
1083
+ const { tileW, tileH } = this.liveTile();
1084
+ const centre = this.getCenterPoint();
1085
+ const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1086
+ return new import_fabric2.Point(centre.x - shift.x, centre.y - shift.y);
1087
+ }
1035
1088
  _render(ctx) {
1036
- const width = this.width ?? 0;
1037
- const height = this.height ?? 0;
1089
+ const { width, height } = this.area;
1038
1090
  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);
1091
+ const { tileW, tileH } = this.liveTile();
1092
+ const scale = contextScale(ctx);
1093
+ const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
1041
1094
  if (!snapshot) return;
1042
- const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
1095
+ const centre = this.getCenterPoint();
1096
+ const angle = this.liveAngle();
1043
1097
  ctx.save();
1044
- ctx.translate(-width / 2, -height / 2);
1045
- drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
1098
+ ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
1099
+ ctx.rotate(-angle * Math.PI / 180);
1100
+ ctx.translate(-centre.x, -centre.y);
1101
+ const shift = this.shiftVector(tileW, tileH, angle);
1102
+ const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1103
+ const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1104
+ drawTiles(
1105
+ ctx,
1106
+ snapshot,
1107
+ config,
1108
+ width,
1109
+ height,
1110
+ tileW,
1111
+ tileH,
1112
+ origin,
1113
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
1114
+ );
1046
1115
  ctx.restore();
1047
1116
  }
1048
1117
  /** Raster fallback for SVG export — one `<image>` covering the print area. */
1049
1118
  _toSVG() {
1050
- const width = this.width ?? 0;
1051
- const height = this.height ?? 0;
1119
+ const { width, height } = this.area;
1052
1120
  if (width <= 0 || height <= 0) return [];
1053
1121
  const el = document.createElement("canvas");
1054
1122
  el.width = Math.max(1, Math.round(width));
1055
1123
  el.height = Math.max(1, Math.round(height));
1056
1124
  const ctx = el.getContext("2d");
1057
1125
  if (!ctx) return [];
1058
- const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
1059
- const snapshot = this.ensureSnapshot(tileScale);
1126
+ const { tileW, tileH } = this.liveTile();
1127
+ const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
1060
1128
  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());
1129
+ const centre = this.getCenterPoint();
1130
+ const angle = this.liveAngle();
1131
+ const shift = this.shiftVector(tileW, tileH, angle);
1132
+ drawTiles(
1133
+ ctx,
1134
+ snapshot,
1135
+ { ...this.config, angle, offsetX: 0, offsetY: 0 },
1136
+ width,
1137
+ height,
1138
+ tileW,
1139
+ tileH,
1140
+ { x: centre.x - shift.x, y: centre.y - shift.y },
1141
+ // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1142
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1143
+ );
1063
1144
  return [
1064
- `<image x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" `,
1065
- `xlink:href="${el.toDataURL("image/png")}"></image>
1145
+ `<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
1146
+ `<image x="0" y="0" width="${width}" height="${height}" `,
1147
+ `xlink:href="${el.toDataURL("image/png")}"></image></g>
1066
1148
  `
1067
1149
  ];
1068
1150
  }
1151
+ /**
1152
+ * The proxy holds a live reference to its source, which would make a
1153
+ * serialized canvas circular. Nothing persists this object (only layers are
1154
+ * serialized) — this keeps an accidental `canvas.toObject()` from throwing.
1155
+ */
1156
+ toObject() {
1157
+ const plain = super.toObject();
1158
+ delete plain.source;
1159
+ return plain;
1160
+ }
1069
1161
  /**
1070
1162
  * Fabric's generic clone round-trips through `toObject()` + the class
1071
1163
  * registry, which cannot carry a live source reference. Export paths clone
@@ -1073,46 +1165,110 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1073
1165
  * tiling. The copy shares the source (it only ever reads from it).
1074
1166
  */
1075
1167
  clone() {
1076
- const copy = new _TiledPatternObject(
1077
- this.source,
1078
- this.config,
1079
- this.width ?? 0,
1080
- this.height ?? 0
1081
- );
1168
+ const copy = new _TiledPatternObject(this.source, this.config, this.area);
1082
1169
  copy.set({
1083
1170
  left: this.left,
1084
1171
  top: this.top,
1172
+ angle: this.angle,
1173
+ scaleX: this.scaleX,
1174
+ scaleY: this.scaleY,
1175
+ width: this.width,
1176
+ height: this.height,
1085
1177
  visible: this.visible,
1086
1178
  opacity: this.opacity
1087
1179
  });
1088
1180
  return Promise.resolve(copy);
1089
1181
  }
1090
1182
  /**
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.
1183
+ * The source's tile footprint: its bounding box, minus a stroke it reserves
1184
+ * room for but never paints.
1185
+ *
1186
+ * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
1187
+ * colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
1188
+ * stroke. Stepping the grid by that box puts a transparent seam between every
1189
+ * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1190
+ * box it is stepped by.
1094
1191
  */
1095
- toObject() {
1096
- const plain = super.toObject();
1097
- delete plain.source;
1098
- return plain;
1192
+ sourceBox() {
1193
+ const rect = this.source.getBoundingRect();
1194
+ if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1195
+ const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
1196
+ const radians = (this.source.angle ?? 0) * Math.PI / 180;
1197
+ const cos = Math.abs(Math.cos(radians));
1198
+ const sin = Math.abs(Math.sin(radians));
1199
+ return {
1200
+ width: dims.x * cos + dims.y * sin,
1201
+ height: dims.x * sin + dims.y * cos
1202
+ };
1203
+ }
1204
+ /** The source's on-canvas width, before the tile scale. */
1205
+ baseW() {
1206
+ return Math.max(1, this.sourceBox().width);
1207
+ }
1208
+ /** The source's on-canvas height, before the tile scale. */
1209
+ baseH() {
1210
+ return Math.max(1, this.sourceBox().height);
1211
+ }
1212
+ /**
1213
+ * Size to paint the snapshot at, for a tile of `tileW × tileH`.
1214
+ *
1215
+ * The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
1216
+ * raster up to whole pixels and pads it further for a shadow. Painting it into
1217
+ * the tile step would squeeze the artwork inside that padding — every tile
1218
+ * shrinks by up to a pixel and the grid shows transparent seams at zero
1219
+ * spacing. So the bitmap is painted at its own footprint, scaled by the same
1220
+ * factor the tile is, and the step stays the source's box.
1221
+ */
1222
+ drawSize(tileW, tileH, bleed = 0) {
1223
+ const snapshot = this.snapshotEl;
1224
+ if (!snapshot || this.snapshotScale <= 0) {
1225
+ return { width: tileW + bleed, height: tileH + bleed };
1226
+ }
1227
+ return {
1228
+ width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
1229
+ height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
1230
+ };
1231
+ }
1232
+ /** Tile size from the config alone, ignoring any in-flight gesture. */
1233
+ baseTile() {
1234
+ const box = this.sourceBox();
1235
+ const scale = Math.max(1, this.config.scale ?? 100) / 100;
1236
+ const floor = this.tileFloor();
1237
+ return {
1238
+ tileW: Math.max(floor, box.width * scale),
1239
+ tileH: Math.max(floor, box.height * scale)
1240
+ };
1099
1241
  }
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);
1242
+ /** Tile size as drawn right now, including a live scale gesture. */
1243
+ liveTile() {
1244
+ const base = this.baseTile();
1245
+ const floor = this.tileFloor();
1103
1246
  return {
1104
- tileW: Math.max(floor, snapshot.width / this.snapshotScale * tileScale),
1105
- tileH: Math.max(floor, snapshot.height / this.snapshotScale * tileScale)
1247
+ tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
1248
+ tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
1106
1249
  };
1107
1250
  }
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) };
1251
+ /** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
1252
+ tileFloor() {
1253
+ const { width, height } = this.area;
1254
+ return Math.max(2, Math.sqrt(width * width + height * height) / 200);
1255
+ }
1256
+ /** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
1257
+ shiftVector(tileW, tileH, angle) {
1258
+ const dx = tileW * (clampPercent(this.config.offsetX) / 100);
1259
+ const dy = tileH * (clampPercent(this.config.offsetY) / 100);
1260
+ const radians = angle * Math.PI / 180;
1261
+ const cos = Math.cos(radians);
1262
+ const sin = Math.sin(radians);
1263
+ return new import_fabric2.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1264
+ }
1265
+ anchorFromOrigin(origin, tileW, tileH, angle) {
1266
+ const shift = this.shiftVector(tileW, tileH, angle);
1267
+ return new import_fabric2.Point(origin.x + shift.x, origin.y + shift.y);
1112
1268
  }
1113
1269
  /**
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.
1270
+ * Snapshot the source at (at least) `scale`, reusing the cached one while it
1271
+ * is still sharp enough and the source has not changed.
1116
1272
  */
1117
1273
  ensureSnapshot(scale) {
1118
1274
  const wanted = this.clampScale(scale);
@@ -1143,6 +1299,16 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1143
1299
  return Math.max(0.05, Math.min(requested, budgeted));
1144
1300
  }
1145
1301
  };
1302
+ function paintsStroke(object) {
1303
+ if (!object.strokeWidth || object.strokeWidth <= 0) return false;
1304
+ const { stroke } = object;
1305
+ if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
1306
+ return stroke !== null && stroke !== void 0;
1307
+ }
1308
+ function clampPercent(value) {
1309
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1310
+ return Math.max(-100, Math.min(100, value));
1311
+ }
1146
1312
  function contextScale(ctx) {
1147
1313
  if (typeof ctx.getTransform !== "function") return 1;
1148
1314
  try {
@@ -1154,13 +1320,15 @@ function contextScale(ctx) {
1154
1320
  }
1155
1321
 
1156
1322
  // src/pattern/pattern-manager.ts
1323
+ var MIN_TILE_SCALE = 10;
1324
+ var MAX_TILE_SCALE = 300;
1157
1325
  var PatternManager = class {
1158
1326
  constructor(canvas, layers, history, events) {
1159
1327
  this.canvas = canvas;
1160
1328
  this.layers = layers;
1161
1329
  this.history = history;
1162
1330
  this.events = events;
1163
- this.canvas.on("object:modified", this.onSourceModified);
1331
+ this.canvas.on("object:modified", this.onObjectModified);
1164
1332
  this.canvas.on("text:changed", this.onSourceModified);
1165
1333
  this.events.on("layer:removed", this.onLayerRemoved);
1166
1334
  }
@@ -1169,6 +1337,11 @@ var PatternManager = class {
1169
1337
  history;
1170
1338
  events;
1171
1339
  proxies = /* @__PURE__ */ new Map();
1340
+ onObjectModified = (event) => {
1341
+ const target = event.target;
1342
+ if (target instanceof TiledPatternObject) this.commitGesture(target);
1343
+ else this.invalidateFor(target);
1344
+ };
1172
1345
  onSourceModified = (event) => {
1173
1346
  this.invalidateFor(event.target);
1174
1347
  };
@@ -1210,7 +1383,11 @@ var PatternManager = class {
1210
1383
  try {
1211
1384
  this.detach(layerId);
1212
1385
  restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
1213
- layer.fabricObject.set({ opacity: layer.opacity });
1386
+ layer.fabricObject.set({
1387
+ opacity: layer.opacity,
1388
+ selectable: !layer.locked,
1389
+ evented: !layer.locked
1390
+ });
1214
1391
  this.layers.setMeta(layerId, { pattern: void 0 });
1215
1392
  this.canvas.requestRenderAll();
1216
1393
  this.history.save();
@@ -1242,12 +1419,8 @@ var PatternManager = class {
1242
1419
  }
1243
1420
  /** Re-fit every proxy to the print area (canvas resize). */
1244
1421
  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
- }
1422
+ const area = this.area();
1423
+ for (const proxy of this.proxies.values()) proxy.setArea(area);
1251
1424
  if (this.proxies.size > 0) this.canvas.requestRenderAll();
1252
1425
  }
1253
1426
  /** Drop a layer's cached source snapshot (its content changed). */
@@ -1255,6 +1428,7 @@ var PatternManager = class {
1255
1428
  const proxy = this.proxies.get(layerId);
1256
1429
  if (!proxy) return;
1257
1430
  proxy.invalidate();
1431
+ proxy.syncBox();
1258
1432
  this.canvas.requestRenderAll();
1259
1433
  }
1260
1434
  /** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
@@ -1264,7 +1438,7 @@ var PatternManager = class {
1264
1438
  this.attach(layer, state.config);
1265
1439
  }
1266
1440
  dispose() {
1267
- this.canvas.off("object:modified", this.onSourceModified);
1441
+ this.canvas.off("object:modified", this.onObjectModified);
1268
1442
  this.canvas.off("text:changed", this.onSourceModified);
1269
1443
  this.events.off("layer:removed", this.onLayerRemoved);
1270
1444
  this.clearProxies();
@@ -1275,17 +1449,38 @@ var PatternManager = class {
1275
1449
  this.proxies.clear();
1276
1450
  }
1277
1451
  attach(layer, config) {
1278
- const proxy = new TiledPatternObject(
1279
- layer.fabricObject,
1280
- config,
1281
- this.canvas.getWidth(),
1282
- this.canvas.getHeight()
1283
- );
1284
- layer.fabricObject.set({ opacity: 0 });
1452
+ const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
1453
+ const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
1454
+ layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
1285
1455
  this.proxies.set(layer.id, proxy);
1286
1456
  this.layers.setRenderProxy(layer.id, proxy);
1457
+ if (wasActive) this.canvas.setActiveObject(proxy);
1287
1458
  this.canvas.requestRenderAll();
1288
1459
  }
1460
+ /**
1461
+ * Fold a finished drag / scale / rotate on the proxy back into the pattern:
1462
+ * position becomes the grid origin (carried by the source), scale becomes the
1463
+ * tile scale, rotation becomes the pattern angle. Read the live transform
1464
+ * first — writing the config re-fits the box and destroys it.
1465
+ */
1466
+ commitGesture(proxy) {
1467
+ const layer = this.layers.findByObject(proxy);
1468
+ const state = layer?.meta.pattern;
1469
+ if (!layer || !state) return;
1470
+ const origin = proxy.liveOrigin();
1471
+ const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
1472
+ const angle = normalizeAngle(proxy.liveAngle());
1473
+ layer.fabricObject.setPositionByOrigin(origin, "center", "center");
1474
+ layer.fabricObject.setCoords();
1475
+ const config = { ...state.config, scale, angle };
1476
+ this.layers.setMeta(layer.id, { pattern: { ...state, config } });
1477
+ proxy.setConfig(config);
1478
+ this.canvas.requestRenderAll();
1479
+ this.history.save();
1480
+ }
1481
+ area() {
1482
+ return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
1483
+ }
1289
1484
  detach(layerId) {
1290
1485
  const proxy = this.proxies.get(layerId);
1291
1486
  if (!proxy) {
@@ -1302,6 +1497,15 @@ var PatternManager = class {
1302
1497
  if (layer) this.invalidate(layer.id);
1303
1498
  }
1304
1499
  };
1500
+ function clamp2(value, min, max) {
1501
+ if (!Number.isFinite(value)) return min;
1502
+ return Math.max(min, Math.min(max, value));
1503
+ }
1504
+ function normalizeAngle(angle) {
1505
+ if (!Number.isFinite(angle)) return 0;
1506
+ const wrapped = (angle % 360 + 360) % 360;
1507
+ return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
1508
+ }
1305
1509
  function isLegacyState(state) {
1306
1510
  return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
1307
1511
  }
@@ -2852,6 +3056,737 @@ var MaskController = class {
2852
3056
  }
2853
3057
  };
2854
3058
 
3059
+ // src/masks/manager.ts
3060
+ var import_fabric16 = require("fabric");
3061
+
3062
+ // src/masks/compose.ts
3063
+ var import_fabric10 = require("fabric");
3064
+ var MODE_OPERATION = {
3065
+ add: "source-over",
3066
+ subtract: "destination-out",
3067
+ intersect: "destination-in"
3068
+ };
3069
+ function neutralize(child) {
3070
+ child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3071
+ }
3072
+ function baseRect(box) {
3073
+ return new import_fabric10.Rect({
3074
+ left: box.left,
3075
+ top: box.top,
3076
+ width: Math.max(1, box.width),
3077
+ height: Math.max(1, box.height),
3078
+ originX: "left",
3079
+ originY: "top",
3080
+ fill: "#000000",
3081
+ objectCaching: false
3082
+ });
3083
+ }
3084
+ function composeMaskGroup(children, entries, options) {
3085
+ if (children.length === 0) return void 0;
3086
+ children.forEach((child, index) => {
3087
+ const entry = entries[index];
3088
+ child.set({ objectCaching: false });
3089
+ if (!entry || !entry.visible) {
3090
+ neutralize(child);
3091
+ return;
3092
+ }
3093
+ child.set({
3094
+ opacity: entry.opacity,
3095
+ globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
3096
+ });
3097
+ });
3098
+ const first = entries.find((entry) => entry.visible);
3099
+ const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3100
+ return new import_fabric10.Group(withBase, {
3101
+ absolutePositioned: options.absolute,
3102
+ // Cached, so the children's compositing operations resolve against each
3103
+ // other instead of against the page underneath the mask.
3104
+ objectCaching: true,
3105
+ subTargetCheck: false,
3106
+ interactive: false
3107
+ });
3108
+ }
3109
+ function needsAbsoluteSpace(entries) {
3110
+ return entries.some((entry) => !entry.linked);
3111
+ }
3112
+
3113
+ // src/masks/edit.ts
3114
+ var import_fabric12 = require("fabric");
3115
+
3116
+ // src/masks/space.ts
3117
+ var import_fabric11 = require("fabric");
3118
+ function matrixOf(object) {
3119
+ return object.calcTransformMatrix();
3120
+ }
3121
+ function applyMatrix(object, matrix) {
3122
+ const decomposed = import_fabric11.util.qrDecompose(matrix);
3123
+ object.set({
3124
+ flipX: false,
3125
+ flipY: false,
3126
+ originX: "center",
3127
+ originY: "center",
3128
+ left: decomposed.translateX,
3129
+ top: decomposed.translateY,
3130
+ scaleX: decomposed.scaleX,
3131
+ scaleY: decomposed.scaleY,
3132
+ angle: decomposed.angle,
3133
+ skewX: decomposed.skewX,
3134
+ skewY: 0
3135
+ });
3136
+ object.setCoords();
3137
+ }
3138
+ function toCanvasSpace(object, host) {
3139
+ applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3140
+ }
3141
+ function toHostSpace(object, host) {
3142
+ applyMatrix(
3143
+ object,
3144
+ import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object))
3145
+ );
3146
+ }
3147
+ function relativeMatrix(object, host) {
3148
+ return import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object));
3149
+ }
3150
+ function applyRelativeMatrix(object, host, rel) {
3151
+ applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), rel));
3152
+ }
3153
+ function asObject(clip) {
3154
+ return clip;
3155
+ }
3156
+ function toMatrix(values) {
3157
+ if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3158
+ return null;
3159
+ return [values[0], values[1], values[2], values[3], values[4], values[5]];
3160
+ }
3161
+ function fitToBox(object, box, zoom = 1) {
3162
+ const width = Math.max(1, box.width) * zoom;
3163
+ const height = Math.max(1, box.height) * zoom;
3164
+ object.set({
3165
+ originX: "center",
3166
+ originY: "center",
3167
+ angle: 0,
3168
+ skewX: 0,
3169
+ skewY: 0,
3170
+ left: box.left + box.width / 2,
3171
+ top: box.top + box.height / 2,
3172
+ scaleX: width / Math.max(1, object.width ?? 1),
3173
+ scaleY: height / Math.max(1, object.height ?? 1)
3174
+ });
3175
+ object.setCoords();
3176
+ }
3177
+ function unwrapGroup(group) {
3178
+ const children = group.removeAll();
3179
+ for (const child of children) child.setCoords();
3180
+ return children;
3181
+ }
3182
+
3183
+ // src/masks/edit.ts
3184
+ var MaskEditController = class {
3185
+ constructor(canvas, onCommit) {
3186
+ this.canvas = canvas;
3187
+ this.onCommit = onCommit;
3188
+ }
3189
+ canvas;
3190
+ onCommit;
3191
+ handle = null;
3192
+ editing = null;
3193
+ child = null;
3194
+ group = null;
3195
+ active() {
3196
+ return this.editing;
3197
+ }
3198
+ /** Put a handle on the canvas for `child`, an object inside `group`. */
3199
+ async begin(edit, group, child) {
3200
+ this.end();
3201
+ const handle = await child.clone();
3202
+ applyMatrix(handle, matrixOf(child));
3203
+ handle.set({
3204
+ // Outline only: the mask's own fill would sit over the artwork it is
3205
+ // supposed to be revealing.
3206
+ fill: "rgba(0,0,0,0.001)",
3207
+ stroke: "#e0b055",
3208
+ strokeWidth: 2,
3209
+ strokeDashArray: [6, 4],
3210
+ strokeUniform: true,
3211
+ opacity: 1,
3212
+ selectable: true,
3213
+ evented: true,
3214
+ hasControls: true,
3215
+ hasBorders: true,
3216
+ objectCaching: false,
3217
+ excludeFromExport: true
3218
+ });
3219
+ this.handle = handle;
3220
+ this.editing = edit;
3221
+ this.child = child;
3222
+ this.group = group;
3223
+ this.canvas.add(handle);
3224
+ this.canvas.setActiveObject(handle);
3225
+ this.canvas.on("object:moving", this.onTransform);
3226
+ this.canvas.on("object:scaling", this.onTransform);
3227
+ this.canvas.on("object:rotating", this.onTransform);
3228
+ this.canvas.on("object:modified", this.onModified);
3229
+ this.canvas.requestRenderAll();
3230
+ return true;
3231
+ }
3232
+ /** Take the handle down. The geometry it wrote is already in the clip. */
3233
+ end() {
3234
+ if (!this.handle) return;
3235
+ this.canvas.off("object:moving", this.onTransform);
3236
+ this.canvas.off("object:scaling", this.onTransform);
3237
+ this.canvas.off("object:rotating", this.onTransform);
3238
+ this.canvas.off("object:modified", this.onModified);
3239
+ if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
3240
+ this.canvas.remove(this.handle);
3241
+ this.handle = null;
3242
+ this.editing = null;
3243
+ this.child = null;
3244
+ this.group = null;
3245
+ this.canvas.requestRenderAll();
3246
+ }
3247
+ dispose() {
3248
+ this.end();
3249
+ }
3250
+ onTransform = (event) => {
3251
+ if (!this.handle || event.target !== this.handle) return;
3252
+ this.write();
3253
+ };
3254
+ onModified = (event) => {
3255
+ if (!this.handle || event.target !== this.handle) return;
3256
+ this.write();
3257
+ this.onCommit();
3258
+ };
3259
+ /** Handle transform (canvas space) → clip child transform (group-relative). */
3260
+ write() {
3261
+ if (!this.handle || !this.child || !this.group) return;
3262
+ applyMatrix(
3263
+ this.child,
3264
+ import_fabric12.util.multiplyTransformMatrices(
3265
+ import_fabric12.util.invertTransform(matrixOf(this.group)),
3266
+ matrixOf(this.handle)
3267
+ )
3268
+ );
3269
+ this.group.dirty = true;
3270
+ this.group.set({ dirty: true });
3271
+ this.canvas.requestRenderAll();
3272
+ }
3273
+ };
3274
+
3275
+ // src/masks/store.ts
3276
+ var import_fabric15 = require("fabric");
3277
+
3278
+ // src/masks/host.ts
3279
+ var import_fabric13 = require("fabric");
3280
+ function findCanvasHost(layers) {
3281
+ return layers.getAll().find((layer) => layer.meta.canvasMask);
3282
+ }
3283
+ function createCanvasHost(canvas, layers) {
3284
+ const rect = new import_fabric13.Rect({
3285
+ left: 0,
3286
+ top: 0,
3287
+ width: canvas.getWidth(),
3288
+ height: canvas.getHeight(),
3289
+ originX: "left",
3290
+ originY: "top",
3291
+ fill: "#000000",
3292
+ globalCompositeOperation: "destination-in",
3293
+ // It is edited from the layer list, never on the canvas: a drag box on
3294
+ // something that cannot be dragged only reads as broken.
3295
+ selectable: false,
3296
+ evented: false,
3297
+ hasControls: false,
3298
+ hasBorders: false,
3299
+ objectCaching: false
3300
+ });
3301
+ const layer = layers.add("mask", rect, "Design mask");
3302
+ layer.meta.canvasMask = true;
3303
+ layers.setLocked(layer.id, true);
3304
+ return layer;
3305
+ }
3306
+ function pinCanvasHost(layers) {
3307
+ const all = layers.getAll();
3308
+ const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
3309
+ if (hostIndex === -1) return false;
3310
+ let lastContent = -1;
3311
+ all.forEach((layer, index) => {
3312
+ if (layer.type !== "mask") lastContent = index;
3313
+ });
3314
+ if (hostIndex >= lastContent) return false;
3315
+ layers.reorder(all[hostIndex].id, all.length - 1);
3316
+ return true;
3317
+ }
3318
+ function hostBoxOf(canvas, host, absolute) {
3319
+ if (!host) {
3320
+ return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
3321
+ }
3322
+ if (absolute) {
3323
+ host.setCoords();
3324
+ const rect = host.getBoundingRect();
3325
+ return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
3326
+ }
3327
+ const width = Math.max(1, host.width ?? 1);
3328
+ const height = Math.max(1, host.height ?? 1);
3329
+ return { left: -width / 2, top: -height / 2, width, height };
3330
+ }
3331
+
3332
+ // src/masks/install.ts
3333
+ var import_fabric14 = require("fabric");
3334
+ function convertSpace(host, sources, absolute) {
3335
+ const wasAbsolute = host.clipPath instanceof import_fabric14.Group ? host.clipPath.absolutePositioned : absolute;
3336
+ if (absolute === wasAbsolute) return;
3337
+ for (const source of sources) {
3338
+ if (absolute) toCanvasSpace(source, host);
3339
+ else toHostSpace(source, host);
3340
+ }
3341
+ }
3342
+ function withRelativeTransforms(entries, sources, host, absolute) {
3343
+ return entries.map((entry, index) => {
3344
+ const source = sources[index];
3345
+ if (absolute && entry.linked && source) {
3346
+ return { ...entry, rel: [...relativeMatrix(source, host)] };
3347
+ }
3348
+ if (!entry.rel) return entry;
3349
+ const rest = { ...entry };
3350
+ delete rest.rel;
3351
+ return rest;
3352
+ });
3353
+ }
3354
+ function installClip(host, entries, sources, box, absolute) {
3355
+ host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
3356
+ host.dirty = true;
3357
+ host.setCoords();
3358
+ }
3359
+
3360
+ // src/masks/store.ts
3361
+ var CANVAS_MASK_TARGET = "canvas";
3362
+ var DEFAULT_ENTRY = {
3363
+ mode: "add",
3364
+ linked: true,
3365
+ visible: true,
3366
+ opacity: 1
3367
+ };
3368
+ var MaskStackStore = class {
3369
+ constructor(canvas, layers, history, events) {
3370
+ this.canvas = canvas;
3371
+ this.layers = layers;
3372
+ this.history = history;
3373
+ this.events = events;
3374
+ }
3375
+ canvas;
3376
+ layers;
3377
+ history;
3378
+ events;
3379
+ selected = null;
3380
+ pinning = false;
3381
+ list(target) {
3382
+ return this.hostLayer(target)?.meta.maskStack ?? [];
3383
+ }
3384
+ get(target, maskId) {
3385
+ return this.list(target).find((entry) => entry.id === maskId);
3386
+ }
3387
+ /** Every target that currently carries at least one mask. */
3388
+ targets() {
3389
+ return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
3390
+ }
3391
+ /** The box a mask is fitted to, in the space the stack is composed in. */
3392
+ hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
3393
+ return hostBoxOf(this.canvas, this.host(target), absolute);
3394
+ }
3395
+ /** The host layer of a target, optionally creating the design overlay. */
3396
+ hostLayer(target, create = false) {
3397
+ if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
3398
+ const existing = findCanvasHost(this.layers);
3399
+ if (existing || !create) return existing;
3400
+ return createCanvasHost(this.canvas, this.layers);
3401
+ }
3402
+ host(target, create = false) {
3403
+ return this.hostLayer(target, create)?.fabricObject ?? null;
3404
+ }
3405
+ /** Re-pin the design overlay, guarding the reorder that re-triggers this. */
3406
+ pin() {
3407
+ if (this.pinning) return;
3408
+ this.pinning = true;
3409
+ try {
3410
+ pinCanvasHost(this.layers);
3411
+ } finally {
3412
+ this.pinning = false;
3413
+ }
3414
+ }
3415
+ /**
3416
+ * Take the current geometry back out of the composed clip, entry-aligned.
3417
+ *
3418
+ * The entry list is the authority on how to read the clip: with no entries the
3419
+ * clip predates the stack (a mask preset, or a single `clipPath` an older host
3420
+ * installed) and is one mask whole — including when it happens to be a group,
3421
+ * which is why this cannot just unwrap anything group-shaped.
3422
+ */
3423
+ unwrap(target, host) {
3424
+ const clip = host.clipPath;
3425
+ if (!clip) return [];
3426
+ const entries = this.list(target);
3427
+ if (entries.length === 0 || !(clip instanceof import_fabric15.Group)) return [asObject(clip)];
3428
+ const children = unwrapGroup(clip);
3429
+ const extra = children.length - entries.length;
3430
+ return extra > 0 ? children.slice(extra) : children;
3431
+ }
3432
+ /**
3433
+ * Entries for a stack, adopting a pre-stack clip as the first one. Without
3434
+ * this, the first `add()` on an already-masked layer would compose a clip it
3435
+ * has no entry for and silently throw that mask away.
3436
+ */
3437
+ entriesFor(target, sources) {
3438
+ const existing = this.list(target);
3439
+ if (existing.length > 0 || sources.length !== 1) return [...existing];
3440
+ const layer = this.hostLayer(target);
3441
+ const preset = layer?.meta.maskPreset;
3442
+ if (layer) {
3443
+ delete layer.meta.maskPreset;
3444
+ }
3445
+ return [
3446
+ {
3447
+ ...DEFAULT_ENTRY,
3448
+ id: generateId(),
3449
+ name: typeof preset === "string" ? preset : "Mask",
3450
+ linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
3451
+ }
3452
+ ];
3453
+ }
3454
+ /**
3455
+ * Install a stack: convert geometry into the space the stack needs, compose the
3456
+ * clip, store the entries, and commit one history checkpoint for the lot.
3457
+ */
3458
+ commit(target, host, entries, sources, save = true, forceAbsolute = false) {
3459
+ const layer = this.hostLayer(target);
3460
+ if (!layer) return;
3461
+ const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
3462
+ convertSpace(host, sources, absolute);
3463
+ const next = withRelativeTransforms(entries, sources, host, absolute);
3464
+ installClip(host, next, sources, this.hostBox(target, absolute), absolute);
3465
+ if (next.length === 0) {
3466
+ delete layer.meta.maskStack;
3467
+ if (layer.meta.canvasMask) this.layers.remove(layer.id);
3468
+ } else {
3469
+ layer.meta.maskStack = next;
3470
+ }
3471
+ this.canvas.requestRenderAll();
3472
+ this.events.emit("masks:changed", { target });
3473
+ this.events.emit("layer:modified", { layerId: layer.id });
3474
+ if (save) this.history.save();
3475
+ }
3476
+ };
3477
+
3478
+ // src/masks/manager.ts
3479
+ var LayerMaskManager = class extends MaskStackStore {
3480
+ // Committing mid-drag would recompose the group the handle writes into and
3481
+ // leave it pointing at a discarded object; the geometry is settled on endEdit.
3482
+ edits = new MaskEditController(this.canvas, () => this.history.save());
3483
+ onLayersChanged = () => this.pin();
3484
+ // Selecting a layer means the user has moved on from the mask they had open;
3485
+ // leaving both selected would show mask controls for an unrelated layer.
3486
+ onLayerSelected = () => {
3487
+ if (this.selected) this.select(null, null);
3488
+ };
3489
+ onObjectModified = (event) => {
3490
+ const object = event.target;
3491
+ if (!object) return;
3492
+ const layer = this.layers.findByObject(object);
3493
+ if (layer) this.reflow(layer.id);
3494
+ };
3495
+ constructor(canvas, layers, history, events) {
3496
+ super(canvas, layers, history, events);
3497
+ this.events.on("layers:changed", this.onLayersChanged);
3498
+ this.events.on("layer:selected", this.onLayerSelected);
3499
+ this.canvas.on("object:modified", this.onObjectModified);
3500
+ }
3501
+ dispose() {
3502
+ this.edits.dispose();
3503
+ this.events.off("layers:changed", this.onLayersChanged);
3504
+ this.events.off("layer:selected", this.onLayerSelected);
3505
+ this.canvas.off("object:modified", this.onObjectModified);
3506
+ this.selected = null;
3507
+ }
3508
+ // ─── Geometry editing ────────────────────────────────
3509
+ /** The mask currently being dragged on the canvas, if any. */
3510
+ editing() {
3511
+ return this.edits.active();
3512
+ }
3513
+ /**
3514
+ * Put drag handles on one mask. The stack is composed in canvas space for the
3515
+ * duration — a linked mask would otherwise sit in the host's space, where the
3516
+ * handle's own canvas coordinates mean something else entirely. `endEdit`
3517
+ * returns it to whichever space its entries call for.
3518
+ */
3519
+ async beginEdit(target, maskId) {
3520
+ const host = this.host(target);
3521
+ if (!host) return false;
3522
+ const entries = this.list(target);
3523
+ const index = entries.findIndex((entry) => entry.id === maskId);
3524
+ if (index === -1) return false;
3525
+ this.endEdit(false);
3526
+ this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
3527
+ const group = host.clipPath instanceof import_fabric16.Group ? host.clipPath : null;
3528
+ if (!group) return false;
3529
+ const children = group.getObjects();
3530
+ const child = children[children.length - entries.length + index];
3531
+ if (!child) return false;
3532
+ this.select(target, maskId);
3533
+ return this.edits.begin({ target, maskId }, group, child);
3534
+ }
3535
+ /** Take the handles down and settle the stack back into its own space. */
3536
+ endEdit(save = true) {
3537
+ const editing = this.edits.active();
3538
+ this.edits.end();
3539
+ if (editing) this.rebuild(editing.target, save);
3540
+ }
3541
+ // ─── Selection ───────────────────────────────────────
3542
+ getSelected() {
3543
+ return this.selected;
3544
+ }
3545
+ select(target, maskId) {
3546
+ this.selected = target && maskId ? { target, maskId } : null;
3547
+ this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
3548
+ }
3549
+ // ─── Mutations ───────────────────────────────────────
3550
+ add(target, object, options = {}) {
3551
+ const host = this.host(target, true);
3552
+ if (!host) return null;
3553
+ const sources = this.unwrap(target, host);
3554
+ const entries = this.entriesFor(target, sources);
3555
+ const entry = {
3556
+ ...DEFAULT_ENTRY,
3557
+ id: generateId(),
3558
+ name: options.name ?? "Mask",
3559
+ ...options.mode ? { mode: options.mode } : {},
3560
+ ...options.linked === void 0 ? {} : { linked: options.linked },
3561
+ ...options.meta ? { meta: options.meta } : {}
3562
+ };
3563
+ if (target === CANVAS_MASK_TARGET) entry.linked = false;
3564
+ if (options.fit !== false) {
3565
+ fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
3566
+ }
3567
+ sources.push(object);
3568
+ entries.push(entry);
3569
+ this.commit(target, host, entries, sources);
3570
+ return entry;
3571
+ }
3572
+ /** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
3573
+ replaceGeometry(target, maskId, object, options = {}) {
3574
+ const host = this.host(target);
3575
+ if (!host) return false;
3576
+ const entries = [...this.list(target)];
3577
+ const index = entries.findIndex((entry) => entry.id === maskId);
3578
+ if (index === -1) return false;
3579
+ const sources = this.unwrap(target, host);
3580
+ if (options.fit !== false) {
3581
+ fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
3582
+ }
3583
+ sources[index] = object;
3584
+ entries[index] = {
3585
+ ...entries[index],
3586
+ ...options.name ? { name: options.name } : {},
3587
+ ...options.meta ? { meta: options.meta } : {}
3588
+ };
3589
+ this.commit(target, host, entries, sources);
3590
+ return true;
3591
+ }
3592
+ /**
3593
+ * Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
3594
+ * geometry it already has. This is what a zoom control drives: re-picking the
3595
+ * mask would cost another render of a generated alpha map just to resize it.
3596
+ */
3597
+ refit(target, maskId, options = {}) {
3598
+ const host = this.host(target);
3599
+ if (!host) return false;
3600
+ const entries = [...this.list(target)];
3601
+ const index = entries.findIndex((entry) => entry.id === maskId);
3602
+ if (index === -1) return false;
3603
+ const sources = this.unwrap(target, host);
3604
+ const source = sources[index];
3605
+ if (!source) return false;
3606
+ fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
3607
+ this.commit(target, host, entries, sources, options.save ?? true);
3608
+ return true;
3609
+ }
3610
+ remove(target, maskId) {
3611
+ const host = this.host(target);
3612
+ if (!host) return false;
3613
+ const entries = [...this.list(target)];
3614
+ const index = entries.findIndex((entry) => entry.id === maskId);
3615
+ if (index === -1) return false;
3616
+ const sources = this.unwrap(target, host);
3617
+ entries.splice(index, 1);
3618
+ sources.splice(index, 1);
3619
+ if (this.selected?.maskId === maskId) this.select(null, null);
3620
+ this.commit(target, host, entries, sources);
3621
+ return true;
3622
+ }
3623
+ clear(target) {
3624
+ const host = this.host(target);
3625
+ if (!host) return false;
3626
+ if (this.list(target).length === 0) return false;
3627
+ if (this.selected?.target === target) this.select(null, null);
3628
+ this.commit(target, host, [], []);
3629
+ return true;
3630
+ }
3631
+ reorder(target, maskId, index) {
3632
+ const host = this.host(target);
3633
+ if (!host) return false;
3634
+ const entries = [...this.list(target)];
3635
+ const from = entries.findIndex((entry2) => entry2.id === maskId);
3636
+ if (from === -1) return false;
3637
+ const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
3638
+ if (from === to) return false;
3639
+ const sources = this.unwrap(target, host);
3640
+ const [entry] = entries.splice(from, 1);
3641
+ const [source] = sources.splice(from, 1);
3642
+ entries.splice(to, 0, entry);
3643
+ sources.splice(to, 0, source);
3644
+ this.commit(target, host, entries, sources);
3645
+ return true;
3646
+ }
3647
+ /** Replace a mask's host metadata (which preset or generator produced it). */
3648
+ setMeta(target, maskId, meta) {
3649
+ return this.patch(target, maskId, () => ({ meta }));
3650
+ }
3651
+ setMode(target, maskId, mode) {
3652
+ return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
3653
+ }
3654
+ setVisible(target, maskId, visible) {
3655
+ return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
3656
+ }
3657
+ setOpacity(target, maskId, opacity) {
3658
+ if (!Number.isFinite(opacity)) return false;
3659
+ const next = Math.max(0, Math.min(1, opacity));
3660
+ return this.patch(
3661
+ target,
3662
+ maskId,
3663
+ (entry) => entry.opacity === next ? null : { opacity: next }
3664
+ );
3665
+ }
3666
+ setName(target, maskId, name) {
3667
+ const trimmed = name.trim();
3668
+ if (!trimmed) return false;
3669
+ return this.patch(
3670
+ target,
3671
+ maskId,
3672
+ (entry) => entry.name === trimmed ? null : { name: trimmed }
3673
+ );
3674
+ }
3675
+ /**
3676
+ * Link or unlink one mask. Unlinking pins it where it currently appears;
3677
+ * re-linking records its position relative to the host so later host moves
3678
+ * carry it along.
3679
+ */
3680
+ setLinked(target, maskId, linked) {
3681
+ if (target === CANVAS_MASK_TARGET) return false;
3682
+ const host = this.host(target);
3683
+ if (!host) return false;
3684
+ const entries = [...this.list(target)];
3685
+ const index = entries.findIndex((entry) => entry.id === maskId);
3686
+ if (index === -1 || entries[index].linked === linked) return false;
3687
+ const sources = this.unwrap(target, host);
3688
+ entries[index] = { ...entries[index], linked };
3689
+ this.commit(target, host, entries, sources);
3690
+ return true;
3691
+ }
3692
+ /**
3693
+ * Consume a layer, turning its artwork into a mask. Without an explicit target
3694
+ * it masks the layer directly beneath it, and the bottom layer masks the whole
3695
+ * design — there is nothing under it to clip.
3696
+ */
3697
+ convertLayer(layerId, target) {
3698
+ const layer = this.layers.get(layerId);
3699
+ if (!layer || layer.meta.canvasMask) return null;
3700
+ const ordered = this.layers.getAll();
3701
+ const index = ordered.findIndex((candidate) => candidate.id === layerId);
3702
+ const below = index > 0 ? ordered[index - 1] : void 0;
3703
+ const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
3704
+ if (resolved === layerId) return null;
3705
+ const object = layer.fabricObject;
3706
+ if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
3707
+ this.history.beginTransaction();
3708
+ try {
3709
+ if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
3710
+ this.canvas.remove(object);
3711
+ this.layers.remove(layerId);
3712
+ const entry = this.add(resolved, object, { name: layer.name, fit: false });
3713
+ return entry ? { target: resolved, entry } : null;
3714
+ } finally {
3715
+ this.history.endTransaction();
3716
+ }
3717
+ }
3718
+ // ─── Rebuild / restore ───────────────────────────────
3719
+ /** Recompose one stack's clip from the geometry it already holds. */
3720
+ rebuild(target, save = false) {
3721
+ const host = this.host(target);
3722
+ if (!host) return;
3723
+ const sources = this.unwrap(target, host);
3724
+ if (sources.length === 0) return;
3725
+ this.commit(target, host, this.entriesFor(target, sources), sources, save);
3726
+ }
3727
+ /** Re-derive linked masks after the host moved (canvas-space stacks only). */
3728
+ reflow(target) {
3729
+ const host = this.host(target);
3730
+ if (!host) return;
3731
+ const entries = this.list(target);
3732
+ if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
3733
+ if (!entries.some((entry) => entry.linked && entry.rel)) return;
3734
+ const sources = this.unwrap(target, host);
3735
+ entries.forEach((entry, index) => {
3736
+ const source = sources[index];
3737
+ const rel = toMatrix(entry.rel);
3738
+ if (!source || !entry.linked || !rel) return;
3739
+ applyRelativeMatrix(source, host, rel);
3740
+ });
3741
+ this.commit(target, host, [...entries], sources);
3742
+ }
3743
+ /**
3744
+ * Adopt a clip this manager did not install — a host's own single `clipPath` —
3745
+ * as a one-entry stack, so it shows up in the UI as a mask row before anything
3746
+ * is added to it. A mask preset is left alone unless asked for by name: it is
3747
+ * still owned by `MaskPresetManager` until a stack operation takes it over.
3748
+ */
3749
+ adopt(target, name) {
3750
+ const layer = this.hostLayer(target);
3751
+ const clip = layer?.fabricObject.clipPath;
3752
+ if (!layer || !clip || layer.meta.pattern) return null;
3753
+ if ((layer.meta.maskStack ?? []).length > 0) return null;
3754
+ const entry = {
3755
+ ...DEFAULT_ENTRY,
3756
+ id: generateId(),
3757
+ name: name ?? "Mask",
3758
+ linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
3759
+ };
3760
+ layer.meta.maskStack = [entry];
3761
+ delete layer.meta.maskPreset;
3762
+ this.rebuild(target);
3763
+ return entry;
3764
+ }
3765
+ /** After a state restore: re-pin the design overlay and recompose every stack. */
3766
+ refreshAll() {
3767
+ this.selected = null;
3768
+ this.pin();
3769
+ for (const layer of this.layers.getAll()) {
3770
+ if ((layer.meta.maskStack ?? []).length === 0) continue;
3771
+ this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
3772
+ }
3773
+ }
3774
+ // ─── Internals ───────────────────────────────────────
3775
+ patch(target, maskId, change) {
3776
+ const host = this.host(target);
3777
+ if (!host) return false;
3778
+ const entries = [...this.list(target)];
3779
+ const index = entries.findIndex((entry) => entry.id === maskId);
3780
+ if (index === -1) return false;
3781
+ const patch = change(entries[index]);
3782
+ if (!patch) return false;
3783
+ const sources = this.unwrap(target, host);
3784
+ entries[index] = { ...entries[index], ...patch };
3785
+ this.commit(target, host, entries, sources);
3786
+ return true;
3787
+ }
3788
+ };
3789
+
2855
3790
  // src/editor.ts
2856
3791
  var MIN_ZOOM = 0.1;
2857
3792
  var MAX_ZOOM = 8;
@@ -2869,6 +3804,8 @@ var CanvasEditor = class {
2869
3804
  patterns;
2870
3805
  curves;
2871
3806
  maskPresets;
3807
+ /** Stacked boolean masks, per layer and for the design as a whole. */
3808
+ layerMasks;
2872
3809
  fonts;
2873
3810
  licensing;
2874
3811
  pages;
@@ -2893,7 +3830,7 @@ var CanvasEditor = class {
2893
3830
  const widthPx = this.units.toPixels(config.width);
2894
3831
  const heightPx = this.units.toPixels(config.height);
2895
3832
  this.designBackground = config.backgroundColor ?? "#ffffff";
2896
- this.canvas = new import_fabric10.Canvas(canvasElement, {
3833
+ this.canvas = new import_fabric17.Canvas(canvasElement, {
2897
3834
  width: widthPx,
2898
3835
  height: heightPx,
2899
3836
  backgroundColor: this.designBackground,
@@ -2917,6 +3854,7 @@ var CanvasEditor = class {
2917
3854
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
2918
3855
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
2919
3856
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
3857
+ this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
2920
3858
  this.setupCanvasEvents();
2921
3859
  this.refreshSelectionStyle();
2922
3860
  this.history.saveImmediate();
@@ -2926,12 +3864,13 @@ var CanvasEditor = class {
2926
3864
  // ─── Layer Operations ────────────────────────────────
2927
3865
  async addImage(url, options) {
2928
3866
  try {
2929
- const img = await import_fabric10.FabricImage.fromURL(
3867
+ const img = await import_fabric17.FabricImage.fromURL(
2930
3868
  url,
2931
3869
  {},
2932
3870
  { originX: "left", originY: "top", ...options }
2933
3871
  );
2934
3872
  const layer = this.layers.add("image", img);
3873
+ this.layers.select(layer.id);
2935
3874
  this.history.save();
2936
3875
  return layer;
2937
3876
  } catch (error) {
@@ -2951,7 +3890,7 @@ var CanvasEditor = class {
2951
3890
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
2952
3891
  const previous = layer.fabricObject;
2953
3892
  try {
2954
- const replacement = await import_fabric10.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
3893
+ const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2955
3894
  replacement.set({
2956
3895
  left: previous.left,
2957
3896
  top: previous.top,
@@ -2980,7 +3919,7 @@ var CanvasEditor = class {
2980
3919
  }
2981
3920
  }
2982
3921
  addText(text, options) {
2983
- const textbox = new import_fabric10.Textbox(text, {
3922
+ const textbox = new import_fabric17.Textbox(text, {
2984
3923
  fontSize: 32,
2985
3924
  fontFamily: "Arial",
2986
3925
  fill: "#000000",
@@ -2991,12 +3930,14 @@ var CanvasEditor = class {
2991
3930
  ...options
2992
3931
  });
2993
3932
  const layer = this.layers.add("text", textbox);
3933
+ this.layers.select(layer.id);
2994
3934
  this.history.save();
2995
3935
  return layer;
2996
3936
  }
2997
3937
  addShape(plugin, options) {
2998
3938
  const obj = plugin.create(options);
2999
3939
  const layer = this.layers.add("shape", obj, plugin.name);
3940
+ this.layers.select(layer.id);
3000
3941
  this.history.save();
3001
3942
  return layer;
3002
3943
  }
@@ -3019,10 +3960,10 @@ var CanvasEditor = class {
3019
3960
  return value === void 0 ? token : escapeXml(value);
3020
3961
  })
3021
3962
  );
3022
- const { objects, options } = await (0, import_fabric10.loadSVGFromString)(resolved);
3963
+ const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
3023
3964
  const validObjects = objects.filter((object) => object !== null);
3024
3965
  if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
3025
- const group = import_fabric10.util.groupSVGElements(validObjects, options);
3966
+ const group = import_fabric17.util.groupSVGElements(validObjects, options);
3026
3967
  group.set({
3027
3968
  left: this.canvas.getWidth() / 2,
3028
3969
  top: this.canvas.getHeight() / 2,
@@ -3030,6 +3971,7 @@ var CanvasEditor = class {
3030
3971
  originY: "center"
3031
3972
  });
3032
3973
  const layer = this.layers.add("template", group, template.name);
3974
+ this.layers.select(layer.id);
3033
3975
  this.history.save();
3034
3976
  return layer;
3035
3977
  }
@@ -3081,7 +4023,7 @@ var CanvasEditor = class {
3081
4023
  opacity: layer.opacity
3082
4024
  });
3083
4025
  this.patterns.attachTo(copy);
3084
- this.canvas.setActiveObject(clone);
4026
+ this.layers.select(copy.id);
3085
4027
  this.canvas.requestRenderAll();
3086
4028
  this.history.save();
3087
4029
  return copy;
@@ -3094,10 +4036,10 @@ var CanvasEditor = class {
3094
4036
  const next = { ...previous, ...adjustments };
3095
4037
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
3096
4038
  image.filters = [
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) })
4039
+ new import_fabric17.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4040
+ new import_fabric17.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4041
+ new import_fabric17.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4042
+ new import_fabric17.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
3101
4043
  ];
3102
4044
  layer.meta.imageAdjustments = next;
3103
4045
  image.applyFilters();
@@ -3114,7 +4056,7 @@ var CanvasEditor = class {
3114
4056
  const childData = children.map((layer) => structuredClone(layer.toData()));
3115
4057
  const objects = children.map((layer) => layer.fabricObject);
3116
4058
  for (const layer of children) this.layers.remove(layer.id);
3117
- const group = new import_fabric10.Group(objects);
4059
+ const group = new import_fabric17.Group(objects);
3118
4060
  const grouped = this.layers.add("group", group, name);
3119
4061
  grouped.meta.groupChildren = childData;
3120
4062
  this.layers.select(grouped.id);
@@ -3129,11 +4071,9 @@ var CanvasEditor = class {
3129
4071
  if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
3130
4072
  const group = grouped.fabricObject;
3131
4073
  return this.history.transaction(() => {
3132
- const transform = group.calcTransformMatrix();
3133
4074
  const objects = group.removeAll();
3134
4075
  this.layers.remove(id);
3135
4076
  const restored = objects.map((object, index) => {
3136
- import_fabric10.util.addTransformToObject(object, transform);
3137
4077
  object.setCoords();
3138
4078
  const data = childData[index];
3139
4079
  const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
@@ -3161,6 +4101,7 @@ var CanvasEditor = class {
3161
4101
  try {
3162
4102
  await deserializeEditor(this, state);
3163
4103
  await this.patterns.rehydrateAll();
4104
+ this.layerMasks.refreshAll();
3164
4105
  } catch (error) {
3165
4106
  if (!managedByHistory) {
3166
4107
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -3189,7 +4130,7 @@ var CanvasEditor = class {
3189
4130
  const layer = this.layers.get(id);
3190
4131
  if (!layer) throw new Error(`Layer not found: ${id}`);
3191
4132
  try {
3192
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric10.FabricImage) {
4133
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
3193
4134
  const image = await layer.fabricObject.clone();
3194
4135
  image.set({
3195
4136
  left: 0,
@@ -3465,7 +4406,7 @@ var CanvasEditor = class {
3465
4406
  return;
3466
4407
  }
3467
4408
  try {
3468
- const image = await import_fabric10.FabricImage.fromURL(
4409
+ const image = await import_fabric17.FabricImage.fromURL(
3469
4410
  url,
3470
4411
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
3471
4412
  { originX: "left", originY: "top" }
@@ -3733,6 +4674,7 @@ var CanvasEditor = class {
3733
4674
  // ─── Cleanup ────────────────────────────────────────
3734
4675
  dispose() {
3735
4676
  this.masks.dispose();
4677
+ this.layerMasks.dispose();
3736
4678
  this.snapping.dispose();
3737
4679
  this.crop.dispose();
3738
4680
  this.history.dispose();
@@ -3856,6 +4798,7 @@ var AnnotationOverlay = class {
3856
4798
  // Annotate the CommonJS export names for ESM import in node:
3857
4799
  0 && (module.exports = {
3858
4800
  AnnotationOverlay,
4801
+ CANVAS_MASK_TARGET,
3859
4802
  CANVAS_SIZE_PRESETS,
3860
4803
  CanvasEditor,
3861
4804
  CropController,
@@ -3868,6 +4811,7 @@ var AnnotationOverlay = class {
3868
4811
  HistoryManager,
3869
4812
  Layer,
3870
4813
  LayerManager,
4814
+ LayerMaskManager,
3871
4815
  LicenseManager,
3872
4816
  MaskController,
3873
4817
  MaskPresetManager,
@@ -3889,6 +4833,7 @@ var AnnotationOverlay = class {
3889
4833
  buildCurvePathData,
3890
4834
  clamp,
3891
4835
  clearTextureMaskCache,
4836
+ composeMaskGroup,
3892
4837
  computeCoverPlacement,
3893
4838
  computePrintAreaClip,
3894
4839
  computeTilePositions,
@@ -3901,11 +4846,13 @@ var AnnotationOverlay = class {
3901
4846
  exportPNG,
3902
4847
  exportPrintArea,
3903
4848
  exportSVG,
4849
+ fitToBox,
3904
4850
  generateId,
3905
4851
  isCssColor,
3906
4852
  isMaskPresetId,
3907
4853
  isShapeMaskId,
3908
4854
  isTextureMaskId,
4855
+ needsAbsoluteSpace,
3909
4856
  readLayerShadow,
3910
4857
  renderTextureMask,
3911
4858
  resetTransform,
@@ -3913,6 +4860,9 @@ var AnnotationOverlay = class {
3913
4860
  round2,
3914
4861
  sanitizeSvg,
3915
4862
  serializeEditor,
3916
- shapeMaskPathData
4863
+ shapeMaskPathData,
4864
+ toCanvasSpace,
4865
+ toHostSpace,
4866
+ unwrapGroup
3917
4867
  });
3918
4868
  //# sourceMappingURL=index.js.map