@overtone-art/canvas-editor-core 0.6.1 → 0.6.3

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
@@ -71,6 +71,7 @@ __export(index_exports, {
71
71
  exportPNG: () => exportPNG,
72
72
  exportPrintArea: () => exportPrintArea,
73
73
  exportSVG: () => exportSVG,
74
+ fitTextWidth: () => fitTextWidth,
74
75
  fitToBox: () => fitToBox,
75
76
  generateId: () => generateId,
76
77
  isCssColor: () => isCssColor,
@@ -96,7 +97,7 @@ __export(index_exports, {
96
97
  module.exports = __toCommonJS(index_exports);
97
98
 
98
99
  // src/editor.ts
99
- var import_fabric19 = require("fabric");
100
+ var import_fabric20 = require("fabric");
100
101
 
101
102
  // src/events.ts
102
103
  var EventEmitter = class {
@@ -294,6 +295,19 @@ var LayerManager = class {
294
295
  this.layers = this.layers.filter((l) => l.id !== group.id);
295
296
  this.syncZOrder();
296
297
  }
298
+ /**
299
+ * Move existing layer records under a group, and announce the new tree.
300
+ * Assigning `group.children` directly emits nothing, and the reorder that
301
+ * follows a grouping is a no-op whenever the group already sits where it
302
+ * belongs — leaving `layers:changed` describing a childless group, so a panel
303
+ * built on it renders the group with nothing inside.
304
+ */
305
+ adoptChildren(group, children) {
306
+ for (const child of children) child.parentId = group.id;
307
+ group.children = children;
308
+ this.emitChanged();
309
+ this.onPropertyChanged?.();
310
+ }
297
311
  /**
298
312
  * Re-attach child records to an enlivened group. `data[i]` describes
299
313
  * `group._objects[i]` — the order fabric serializes and restores them in. A
@@ -1212,10 +1226,63 @@ var CropController = class {
1212
1226
  var STROKE2 = "#22c55e";
1213
1227
 
1214
1228
  // src/pattern/pattern-manager.ts
1215
- var import_fabric5 = require("fabric");
1229
+ var import_fabric6 = require("fabric");
1216
1230
 
1217
1231
  // src/pattern/tiled-pattern-object.ts
1232
+ var import_fabric5 = require("fabric");
1233
+
1234
+ // src/text-fit.ts
1218
1235
  var import_fabric4 = require("fabric");
1236
+ var TEXT_FIT_SLACK = 0.5;
1237
+ var MEASURE_WIDTH = 1e5;
1238
+ function unwrappedWidth(text) {
1239
+ const authored = text.width;
1240
+ try {
1241
+ text.set({ width: MEASURE_WIDTH });
1242
+ text.initDimensions?.();
1243
+ const measured = text.calcTextWidth?.() ?? authored;
1244
+ return Number.isFinite(measured) && measured > 0 ? measured : authored;
1245
+ } finally {
1246
+ text.set({ width: authored });
1247
+ text.initDimensions?.();
1248
+ }
1249
+ }
1250
+ function asText(object) {
1251
+ if (!object) return null;
1252
+ const text = object;
1253
+ return typeof text.text === "string" && typeof text.calcTextWidth === "function" ? text : null;
1254
+ }
1255
+ function textInk(text) {
1256
+ const boxWidth = Math.max(0, text.width ?? 0);
1257
+ const measured = text.calcTextWidth?.() ?? boxWidth;
1258
+ const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));
1259
+ const slack = (boxWidth - width) / 2;
1260
+ const align = text.textAlign ?? "left";
1261
+ if (align.includes("center")) return { width, dx: 0 };
1262
+ const flip = text.flipX ? -1 : 1;
1263
+ return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1264
+ }
1265
+ function fitTextWidth(object) {
1266
+ const text = asText(object);
1267
+ if (!text || text.path) return false;
1268
+ const before = textInk(text);
1269
+ const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1270
+ if (Math.abs(fitted - text.width) < TEXT_FIT_SLACK) return false;
1271
+ const centre = text.getCenterPoint();
1272
+ text.set({ width: fitted });
1273
+ text.initDimensions?.();
1274
+ const after = textInk(text);
1275
+ const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1276
+ const radians = (text.angle ?? 0) * Math.PI / 180;
1277
+ const moved = new import_fabric4.Point(
1278
+ centre.x + shift * Math.cos(radians),
1279
+ centre.y + shift * Math.sin(radians)
1280
+ );
1281
+ text.setPositionByOrigin(moved, "center", "center");
1282
+ text.setCoords();
1283
+ text.dirty = true;
1284
+ return true;
1285
+ }
1219
1286
 
1220
1287
  // src/pattern/tile-geometry.ts
1221
1288
  var MAX_TILES_PER_AXIS = 200;
@@ -1228,13 +1295,13 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
1228
1295
  const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
1229
1296
  const cols = Math.ceil(span / tileW) + 2;
1230
1297
  const rows = Math.ceil(span / tileH) + 2;
1231
- const halfCols = Math.ceil(cols / 2);
1232
- const halfRows = Math.ceil(rows / 2);
1298
+ const [colFrom, colTo] = axisRange(config.horizontalLimit, Math.ceil(cols / 2));
1299
+ const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(rows / 2));
1233
1300
  const shiftX = tileW * (clampOffset(config.offsetX) / 100);
1234
1301
  const shiftY = tileH * (clampOffset(config.offsetY) / 100);
1235
1302
  const placements = [];
1236
- for (let j = -halfRows; j <= halfRows; j++) {
1237
- for (let i = -halfCols; i <= halfCols; i++) {
1303
+ for (let j = rowFrom; j <= rowTo; j++) {
1304
+ for (let i = colFrom; i <= colTo; i++) {
1238
1305
  let x = i * tileW + shiftX;
1239
1306
  let y = j * tileH + shiftY;
1240
1307
  if (config.mode === "brick-horizontal" && mod2(j) === 1) {
@@ -1264,6 +1331,12 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, dra
1264
1331
  }
1265
1332
  ctx.restore();
1266
1333
  }
1334
+ function axisRange(limit, half) {
1335
+ const count = Number.isFinite(limit) ? Math.floor(limit) : 0;
1336
+ if (!count || count <= 0) return [-half, half];
1337
+ const from = -Math.floor((count - 1) / 2);
1338
+ return [from, from + count - 1];
1339
+ }
1267
1340
  function cornerRadius(origin, w, h) {
1268
1341
  const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
1269
1342
  const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
@@ -1282,7 +1355,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
1282
1355
  var MAX_SNAPSHOT_SCALE = 8;
1283
1356
  var SNAPSHOT_SHRINK_FACTOR = 2;
1284
1357
  var TILE_BLEED_DEVICE_PX = 2;
1285
- var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
1358
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.FabricObject {
1286
1359
  static type = "TiledPattern";
1287
1360
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
1288
1361
  source;
@@ -1335,7 +1408,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1335
1408
  */
1336
1409
  syncBox() {
1337
1410
  const { tileW, tileH } = this.baseTile();
1338
- const origin = this.source.getCenterPoint();
1411
+ const origin = this.source.getCenterPoint().add(this.inkOffset());
1339
1412
  const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
1340
1413
  this.set({
1341
1414
  left: anchor.x,
@@ -1365,7 +1438,8 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1365
1438
  const { tileW, tileH } = this.liveTile();
1366
1439
  const centre = this.getCenterPoint();
1367
1440
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1368
- return new import_fabric4.Point(centre.x - shift.x, centre.y - shift.y);
1441
+ const ink = this.inkOffset();
1442
+ return new import_fabric5.Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1369
1443
  }
1370
1444
  _render(ctx) {
1371
1445
  const { width, height } = this.area;
@@ -1380,8 +1454,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1380
1454
  ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
1381
1455
  ctx.rotate(-angle * Math.PI / 180);
1382
1456
  ctx.translate(-centre.x, -centre.y);
1383
- const shift = this.shiftVector(tileW, tileH, angle);
1384
- const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1457
+ const origin = { x: centre.x, y: centre.y };
1385
1458
  const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1386
1459
  drawTiles(
1387
1460
  ctx,
@@ -1410,7 +1483,6 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1410
1483
  if (!snapshot) return [];
1411
1484
  const centre = this.getCenterPoint();
1412
1485
  const angle = this.liveAngle();
1413
- const shift = this.shiftVector(tileW, tileH, angle);
1414
1486
  drawTiles(
1415
1487
  ctx,
1416
1488
  snapshot,
@@ -1419,7 +1491,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1419
1491
  height,
1420
1492
  tileW,
1421
1493
  tileH,
1422
- { x: centre.x - shift.x, y: centre.y - shift.y },
1494
+ { x: centre.x, y: centre.y },
1423
1495
  // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1424
1496
  this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1425
1497
  );
@@ -1462,7 +1534,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1462
1534
  return Promise.resolve(copy);
1463
1535
  }
1464
1536
  /**
1465
- * The source's tile footprint: its bounding box, minus a stroke it reserves
1537
+ * The source's tile footprint: the art it paints, minus a stroke it reserves
1466
1538
  * room for but never paints.
1467
1539
  *
1468
1540
  * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
@@ -1470,11 +1542,18 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1470
1542
  * stroke. Stepping the grid by that box puts a transparent seam between every
1471
1543
  * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1472
1544
  * box it is stepped by.
1545
+ *
1546
+ * Text is the same problem an order of magnitude larger — see {@link textInk}.
1473
1547
  */
1474
1548
  sourceBox() {
1549
+ const text = asText(this.source);
1550
+ const paints = paintsStroke(this.source);
1475
1551
  const rect = this.source.getBoundingRect();
1476
- if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1477
- const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
1552
+ if (paints && !text) return { width: rect.width, height: rect.height };
1553
+ const options = {};
1554
+ if (!paints) options.strokeWidth = 0;
1555
+ if (text) options.width = textInk(text).width;
1556
+ const dims = this.source._getTransformedDimensions(options);
1478
1557
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1479
1558
  const cos = Math.abs(Math.cos(radians));
1480
1559
  const sin = Math.abs(Math.sin(radians));
@@ -1483,6 +1562,22 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1483
1562
  height: dims.x * sin + dims.y * cos
1484
1563
  };
1485
1564
  }
1565
+ /**
1566
+ * Vector from the source's centre to the centre of the art it paints.
1567
+ *
1568
+ * Zero for everything except text that does not fill its box: the tile has to
1569
+ * sit on the glyphs, or the frame the user drags frames empty space and the
1570
+ * grid registers off the run by half the padding.
1571
+ */
1572
+ inkOffset() {
1573
+ const text = asText(this.source);
1574
+ if (!text) return new import_fabric5.Point(0, 0);
1575
+ const { dx } = textInk(text);
1576
+ if (!dx) return new import_fabric5.Point(0, 0);
1577
+ const scaled = dx * (this.source.scaleX ?? 1);
1578
+ const radians = (this.source.angle ?? 0) * Math.PI / 180;
1579
+ return new import_fabric5.Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
1580
+ }
1486
1581
  /** The source's on-canvas width, before the tile scale. */
1487
1582
  baseW() {
1488
1583
  return Math.max(1, this.sourceBox().width);
@@ -1542,11 +1637,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1542
1637
  const radians = angle * Math.PI / 180;
1543
1638
  const cos = Math.cos(radians);
1544
1639
  const sin = Math.sin(radians);
1545
- return new import_fabric4.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1640
+ return new import_fabric5.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1546
1641
  }
1547
1642
  anchorFromOrigin(origin, tileW, tileH, angle) {
1548
1643
  const shift = this.shiftVector(tileW, tileH, angle);
1549
- return new import_fabric4.Point(origin.x + shift.x, origin.y + shift.y);
1644
+ return new import_fabric5.Point(origin.x + shift.x, origin.y + shift.y);
1550
1645
  }
1551
1646
  /**
1552
1647
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1559,7 +1654,15 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1559
1654
  const source = this.source;
1560
1655
  const opacity = source.opacity;
1561
1656
  source.opacity = 1;
1657
+ const text = asText(source);
1658
+ const authoredWidth = text?.width;
1659
+ const fitted = text ? textInk(text).width + TEXT_FIT_SLACK : 0;
1660
+ const hug = text !== null && authoredWidth !== void 0 && fitted < authoredWidth;
1562
1661
  try {
1662
+ if (hug && text) {
1663
+ text.set({ width: fitted });
1664
+ text.initDimensions?.();
1665
+ }
1563
1666
  const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
1564
1667
  if (!el.width || !el.height) return null;
1565
1668
  this.snapshotEl = el;
@@ -1568,6 +1671,10 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1568
1671
  return this.snapshotEl;
1569
1672
  } finally {
1570
1673
  source.opacity = opacity;
1674
+ if (hug && text && authoredWidth !== void 0) {
1675
+ text.set({ width: authoredWidth });
1676
+ text.initDimensions?.();
1677
+ }
1571
1678
  source.dirty = false;
1572
1679
  }
1573
1680
  return this.snapshotEl;
@@ -1806,7 +1913,7 @@ async function unbakeLegacyLayer(layer, state) {
1806
1913
  cropY: state.original.cropY,
1807
1914
  angle: state.original.angle
1808
1915
  });
1809
- image.clipPath = state.originalClip ? (await import_fabric5.util.enlivenObjects([state.originalClip]))[0] : void 0;
1916
+ image.clipPath = state.originalClip ? (await import_fabric6.util.enlivenObjects([state.originalClip]))[0] : void 0;
1810
1917
  restoreLocks(image, state.originalLocks);
1811
1918
  image.setCoords();
1812
1919
  }
@@ -1824,7 +1931,7 @@ function restoreLocks(obj, locks) {
1824
1931
  }
1825
1932
 
1826
1933
  // src/text-curve.ts
1827
- var import_fabric6 = require("fabric");
1934
+ var import_fabric7 = require("fabric");
1828
1935
 
1829
1936
  // src/text-curve-geometry.ts
1830
1937
  var DEFAULT_TEXT_CURVE = {
@@ -1944,17 +2051,55 @@ function buildCurvePathData(config, width, fontSize) {
1944
2051
  }
1945
2052
 
1946
2053
  // src/text-curve.ts
1947
- var MEASURE_WIDTH = 1e5;
2054
+ var MEASURE_WIDTH2 = 1e5;
1948
2055
  var PATH_SLACK = 0.06;
1949
2056
  var FALLBACK_LINE_HEIGHT = 1.16;
1950
2057
  var patches = /* @__PURE__ */ new WeakMap();
2058
+ var dimensionPatches = /* @__PURE__ */ new WeakMap();
2059
+ var authoredCaching = /* @__PURE__ */ new WeakMap();
2060
+ var GLYPH_HALF_HEIGHT = 0.7;
2061
+ function curvedInkSize(text) {
2062
+ const lineCount = text._textLines?.length ?? 0;
2063
+ for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
2064
+ const lines = text.__charBounds;
2065
+ if (!lines?.length) return null;
2066
+ let halfWidth = 0;
2067
+ let halfHeight = 0;
2068
+ for (const line of lines) {
2069
+ for (const box of line ?? []) {
2070
+ const { renderLeft, renderTop } = box;
2071
+ if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
2072
+ const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
2073
+ const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
2074
+ const angle = box.angle ?? 0;
2075
+ const cos = Math.abs(Math.cos(angle));
2076
+ const sin = Math.abs(Math.sin(angle));
2077
+ halfWidth = Math.max(
2078
+ halfWidth,
2079
+ Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
2080
+ );
2081
+ halfHeight = Math.max(
2082
+ halfHeight,
2083
+ Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
2084
+ );
2085
+ }
2086
+ }
2087
+ if (halfWidth === 0 && halfHeight === 0) return null;
2088
+ return { width: halfWidth * 2, height: halfHeight * 2 };
2089
+ }
2090
+ function growBoxToInk(text) {
2091
+ const ink = curvedInkSize(text);
2092
+ if (!ink) return;
2093
+ if (ink.width > (text.width ?? 0)) text.width = ink.width;
2094
+ if (ink.height > (text.height ?? 0)) text.height = ink.height;
2095
+ }
1951
2096
  function isCurvable(object) {
1952
2097
  return !!object && typeof object.text === "string";
1953
2098
  }
1954
2099
  function measureText(text) {
1955
2100
  const authored = text.width;
1956
2101
  try {
1957
- text.set({ width: MEASURE_WIDTH });
2102
+ text.set({ width: MEASURE_WIDTH2 });
1958
2103
  text.initDimensions?.();
1959
2104
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1960
2105
  } finally {
@@ -1971,7 +2116,7 @@ function lineHeightOf(text) {
1971
2116
  return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
1972
2117
  }
1973
2118
  function toFabricPath(curve) {
1974
- return new import_fabric6.Path(curve.data, { visible: false, objectCaching: false });
2119
+ return new import_fabric7.Path(curve.data, { visible: false, objectCaching: false });
1975
2120
  }
1976
2121
  var TextCurveManager = class {
1977
2122
  constructor(canvas, layers, history, events) {
@@ -1979,11 +2124,29 @@ var TextCurveManager = class {
1979
2124
  this.layers = layers;
1980
2125
  this.history = history;
1981
2126
  this.events = events;
2127
+ this.canvas.on("text:changed", this.onTextChanged);
1982
2128
  }
1983
2129
  canvas;
1984
2130
  layers;
1985
2131
  history;
1986
2132
  events;
2133
+ /**
2134
+ * Typing on the canvas changes the run the paths were built for. Fabric drops
2135
+ * a glyph whose centre falls past the end of the path, so without rebuilding
2136
+ * here a longer replacement simply vanishes — select-all-and-retype rendered
2137
+ * an empty box — and a shorter one piles up on a path built for the old text.
2138
+ *
2139
+ * No history entry: fabric records the edit when editing exits, and one save
2140
+ * per keystroke would bury every earlier step under the typing.
2141
+ */
2142
+ onTextChanged = (event) => {
2143
+ const layer = event.target ? this.layers.findByObject(event.target) : void 0;
2144
+ if (!layer?.meta.curve) return;
2145
+ this.refresh(layer.id, false);
2146
+ };
2147
+ dispose() {
2148
+ this.canvas.off("text:changed", this.onTextChanged);
2149
+ }
1987
2150
  /** Curve parameters for a layer, or null when it is not curved text. */
1988
2151
  get(layerId) {
1989
2152
  const layer = this.layers.get(layerId);
@@ -2068,6 +2231,8 @@ var TextCurveManager = class {
2068
2231
  * outside the layer's box — where object caching clips it away.
2069
2232
  */
2070
2233
  attach(text, curves) {
2234
+ if (!authoredCaching.has(text)) authoredCaching.set(text, text.objectCaching);
2235
+ text.set({ objectCaching: false });
2071
2236
  const paths = curves.map(toFabricPath);
2072
2237
  const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
2073
2238
  text.set({
@@ -2079,6 +2244,35 @@ var TextCurveManager = class {
2079
2244
  pathStartOffset: 0
2080
2245
  });
2081
2246
  this.patchLineMeasure(text, paths, union);
2247
+ this.patchDimensions(text);
2248
+ }
2249
+ /**
2250
+ * Re-apply {@link growBoxToInk} after every one of fabric's own dimension
2251
+ * passes. Typing, a font change, a style edit and a state restore all call
2252
+ * `initDimensions`, and each one puts the box back to the path's bounding box
2253
+ * — so growing it once, here, would hold only until the next keystroke.
2254
+ */
2255
+ patchDimensions(text) {
2256
+ if (dimensionPatches.has(text)) return;
2257
+ const original = text.initDimensions;
2258
+ if (typeof original !== "function") return;
2259
+ const installed = function() {
2260
+ original.call(this);
2261
+ growBoxToInk(this);
2262
+ };
2263
+ const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "initDimensions");
2264
+ text.initDimensions = installed;
2265
+ dimensionPatches.set(text, { original, installed, ownedOriginal });
2266
+ }
2267
+ /** Restore fabric's own dimension pass, if this manager replaced it. */
2268
+ unpatchDimensions(text) {
2269
+ const patch = dimensionPatches.get(text);
2270
+ if (!patch) return;
2271
+ if (text.initDimensions === patch.installed) {
2272
+ if (patch.ownedOriginal) text.initDimensions = patch.original;
2273
+ else delete text.initDimensions;
2274
+ }
2275
+ dimensionPatches.delete(text);
2082
2276
  }
2083
2277
  /**
2084
2278
  * Fabric lays every line of a text object along `this.path`, from one
@@ -2130,13 +2324,19 @@ var TextCurveManager = class {
2130
2324
  }
2131
2325
  detach(text, authoredWidth) {
2132
2326
  this.unpatchLineMeasure(text);
2327
+ this.unpatchDimensions(text);
2328
+ const caching = authoredCaching.get(text);
2329
+ if (caching !== void 0) {
2330
+ text.set({ objectCaching: caching });
2331
+ authoredCaching.delete(text);
2332
+ }
2133
2333
  text.set({ path: null, pathStartOffset: 0 });
2134
2334
  if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
2135
2335
  }
2136
2336
  };
2137
2337
 
2138
2338
  // src/mask-presets/manager.ts
2139
- var import_fabric7 = require("fabric");
2339
+ var import_fabric8 = require("fabric");
2140
2340
 
2141
2341
  // src/mask-presets/shapes.ts
2142
2342
  var SHAPE_MASK_IDS = [
@@ -2403,13 +2603,13 @@ var MaskPresetManager = class {
2403
2603
  objectCaching: false
2404
2604
  };
2405
2605
  if (isShapeMaskId(id)) {
2406
- return new import_fabric7.Path(shapeMaskPathData(id), {
2606
+ return new import_fabric8.Path(shapeMaskPathData(id), {
2407
2607
  ...shared,
2408
2608
  scaleX: width / SHAPE_MASK_BOX,
2409
2609
  scaleY: height / SHAPE_MASK_BOX
2410
2610
  });
2411
2611
  }
2412
- return new import_fabric7.FabricImage(renderTextureMask(id), {
2612
+ return new import_fabric8.FabricImage(renderTextureMask(id), {
2413
2613
  ...shared,
2414
2614
  scaleX: width / TEXTURE_MASK_SIZE,
2415
2615
  scaleY: height / TEXTURE_MASK_SIZE
@@ -2418,7 +2618,7 @@ var MaskPresetManager = class {
2418
2618
  };
2419
2619
 
2420
2620
  // src/shadow.ts
2421
- var import_fabric8 = require("fabric");
2621
+ var import_fabric9 = require("fabric");
2422
2622
 
2423
2623
  // src/utils/color.ts
2424
2624
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -2456,7 +2656,7 @@ function applyLayerShadow(object, config) {
2456
2656
  }
2457
2657
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2458
2658
  object.set({
2459
- shadow: new import_fabric8.Shadow({
2659
+ shadow: new import_fabric9.Shadow({
2460
2660
  color,
2461
2661
  blur: Math.max(0, next.blur),
2462
2662
  offsetX: next.offsetX,
@@ -2570,7 +2770,7 @@ var UnitConverter = class {
2570
2770
  };
2571
2771
 
2572
2772
  // src/serialization.ts
2573
- var import_fabric9 = require("fabric");
2773
+ var import_fabric10 = require("fabric");
2574
2774
  var VERSION = "2.0.0";
2575
2775
  function serializeEditor(editor) {
2576
2776
  return {
@@ -2606,7 +2806,7 @@ async function deserializeEditor(editor, state) {
2606
2806
  }
2607
2807
  const staged = await Promise.all(
2608
2808
  state.layers.map(async (serialized) => {
2609
- const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
2809
+ const fabricObject = (await import_fabric10.util.enlivenObjects([serialized.fabricObject]))[0];
2610
2810
  if (!fabricObject) {
2611
2811
  const source = serialized.fabricObject.src;
2612
2812
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2617,7 +2817,7 @@ async function deserializeEditor(editor, state) {
2617
2817
  return { serialized, fabricObject };
2618
2818
  })
2619
2819
  );
2620
- const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
2820
+ const stagedBackground = state.backgroundImage ? (await import_fabric10.util.enlivenObjects([state.backgroundImage]))[0] : null;
2621
2821
  if (state.backgroundImage && !stagedBackground) {
2622
2822
  const source = state.backgroundImage.src;
2623
2823
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2669,7 +2869,7 @@ function restoreLayer(editor, serialized, fabricObject) {
2669
2869
  }
2670
2870
 
2671
2871
  // src/export.ts
2672
- var import_fabric10 = require("fabric");
2872
+ var import_fabric11 = require("fabric");
2673
2873
 
2674
2874
  // src/displacement.ts
2675
2875
  var CHANNEL_INDEX = {
@@ -2763,7 +2963,7 @@ async function exportPNG(canvas, options = {}) {
2763
2963
  }
2764
2964
  async function exportIsolatedPNG(source, objects, options = {}) {
2765
2965
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2766
- const canvas = new import_fabric10.StaticCanvas(element, {
2966
+ const canvas = new import_fabric11.StaticCanvas(element, {
2767
2967
  width: options.width ?? source.getWidth(),
2768
2968
  height: options.height ?? source.getHeight(),
2769
2969
  backgroundColor: options.backgroundColor || void 0
@@ -2793,7 +2993,7 @@ async function exportPrintArea(source, area, options = {}) {
2793
2993
  throw new Error("Print area does not overlap the canvas");
2794
2994
  }
2795
2995
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2796
- const canvas = new import_fabric10.StaticCanvas(element, { width, height });
2996
+ const canvas = new import_fabric11.StaticCanvas(element, { width, height });
2797
2997
  try {
2798
2998
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2799
2999
  if (clones.length) canvas.add(...clones);
@@ -3233,7 +3433,7 @@ var ProjectManager = class {
3233
3433
  };
3234
3434
 
3235
3435
  // src/mask.ts
3236
- var import_fabric11 = require("fabric");
3436
+ var import_fabric12 = require("fabric");
3237
3437
  var MaskRefinementError = class extends Error {
3238
3438
  constructor(code, message, cause) {
3239
3439
  super(message);
@@ -3265,7 +3465,7 @@ var MaskController = class {
3265
3465
  throw new Error("Mask dimensions must be positive integers");
3266
3466
  }
3267
3467
  const backing = this.makeCanvas(width, height);
3268
- const image = new import_fabric11.FabricImage(backing, {
3468
+ const image = new import_fabric12.FabricImage(backing, {
3269
3469
  left: 0,
3270
3470
  top: 0,
3271
3471
  originX: "left",
@@ -3491,10 +3691,10 @@ var MaskController = class {
3491
3691
  };
3492
3692
 
3493
3693
  // src/masks/manager.ts
3494
- var import_fabric18 = require("fabric");
3694
+ var import_fabric19 = require("fabric");
3495
3695
 
3496
3696
  // src/masks/compose.ts
3497
- var import_fabric12 = require("fabric");
3697
+ var import_fabric13 = require("fabric");
3498
3698
  var MODE_OPERATION = {
3499
3699
  add: "source-over",
3500
3700
  subtract: "destination-out",
@@ -3504,7 +3704,7 @@ function neutralize(child) {
3504
3704
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3505
3705
  }
3506
3706
  function baseRect(box) {
3507
- return new import_fabric12.Rect({
3707
+ return new import_fabric13.Rect({
3508
3708
  left: box.left,
3509
3709
  top: box.top,
3510
3710
  width: Math.max(1, box.width),
@@ -3531,7 +3731,7 @@ function composeMaskGroup(children, entries, options) {
3531
3731
  });
3532
3732
  const first = entries.find((entry) => entry.visible);
3533
3733
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3534
- return new import_fabric12.Group(withBase, {
3734
+ return new import_fabric13.Group(withBase, {
3535
3735
  absolutePositioned: options.absolute,
3536
3736
  // Cached, so the children's compositing operations resolve against each
3537
3737
  // other instead of against the page underneath the mask.
@@ -3545,15 +3745,15 @@ function needsAbsoluteSpace(entries) {
3545
3745
  }
3546
3746
 
3547
3747
  // src/masks/edit.ts
3548
- var import_fabric14 = require("fabric");
3748
+ var import_fabric15 = require("fabric");
3549
3749
 
3550
3750
  // src/masks/space.ts
3551
- var import_fabric13 = require("fabric");
3751
+ var import_fabric14 = require("fabric");
3552
3752
  function matrixOf(object) {
3553
3753
  return object.calcTransformMatrix();
3554
3754
  }
3555
3755
  function applyMatrix(object, matrix) {
3556
- const decomposed = import_fabric13.util.qrDecompose(matrix);
3756
+ const decomposed = import_fabric14.util.qrDecompose(matrix);
3557
3757
  object.set({
3558
3758
  flipX: false,
3559
3759
  flipY: false,
@@ -3570,19 +3770,19 @@ function applyMatrix(object, matrix) {
3570
3770
  object.setCoords();
3571
3771
  }
3572
3772
  function toCanvasSpace(object, host) {
3573
- applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3773
+ applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3574
3774
  }
3575
3775
  function toHostSpace(object, host) {
3576
3776
  applyMatrix(
3577
3777
  object,
3578
- import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
3778
+ import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object))
3579
3779
  );
3580
3780
  }
3581
3781
  function relativeMatrix(object, host) {
3582
- return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
3782
+ return import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object));
3583
3783
  }
3584
3784
  function applyRelativeMatrix(object, host, rel) {
3585
- applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
3785
+ applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), rel));
3586
3786
  }
3587
3787
  function asObject(clip) {
3588
3788
  return clip;
@@ -3695,8 +3895,8 @@ var MaskEditController = class {
3695
3895
  if (!this.handle || !this.child || !this.group) return;
3696
3896
  applyMatrix(
3697
3897
  this.child,
3698
- import_fabric14.util.multiplyTransformMatrices(
3699
- import_fabric14.util.invertTransform(matrixOf(this.group)),
3898
+ import_fabric15.util.multiplyTransformMatrices(
3899
+ import_fabric15.util.invertTransform(matrixOf(this.group)),
3700
3900
  matrixOf(this.handle)
3701
3901
  )
3702
3902
  );
@@ -3707,15 +3907,15 @@ var MaskEditController = class {
3707
3907
  };
3708
3908
 
3709
3909
  // src/masks/store.ts
3710
- var import_fabric17 = require("fabric");
3910
+ var import_fabric18 = require("fabric");
3711
3911
 
3712
3912
  // src/masks/host.ts
3713
- var import_fabric15 = require("fabric");
3913
+ var import_fabric16 = require("fabric");
3714
3914
  function findCanvasHost(layers) {
3715
3915
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3716
3916
  }
3717
3917
  function createCanvasHost(canvas, layers) {
3718
- const rect = new import_fabric15.Rect({
3918
+ const rect = new import_fabric16.Rect({
3719
3919
  left: 0,
3720
3920
  top: 0,
3721
3921
  width: canvas.getWidth(),
@@ -3764,9 +3964,9 @@ function hostBoxOf(canvas, host, absolute) {
3764
3964
  }
3765
3965
 
3766
3966
  // src/masks/install.ts
3767
- var import_fabric16 = require("fabric");
3967
+ var import_fabric17 = require("fabric");
3768
3968
  function convertSpace(host, sources, absolute) {
3769
- const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
3969
+ const wasAbsolute = host.clipPath instanceof import_fabric17.Group ? host.clipPath.absolutePositioned : absolute;
3770
3970
  if (absolute === wasAbsolute) return;
3771
3971
  for (const source of sources) {
3772
3972
  if (absolute) toCanvasSpace(source, host);
@@ -3858,7 +4058,7 @@ var MaskStackStore = class {
3858
4058
  const clip = host.clipPath;
3859
4059
  if (!clip) return [];
3860
4060
  const entries = this.list(target);
3861
- if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
4061
+ if (entries.length === 0 || !(clip instanceof import_fabric18.Group)) return [asObject(clip)];
3862
4062
  const children = unwrapGroup(clip);
3863
4063
  const extra = children.length - entries.length;
3864
4064
  return extra > 0 ? children.slice(extra) : children;
@@ -3917,8 +4117,8 @@ var LayerMaskManager = class extends MaskStackStore {
3917
4117
  onLayersChanged = () => this.pin();
3918
4118
  // Selecting a layer means the user has moved on from the mask they had open;
3919
4119
  // leaving both selected would show mask controls for an unrelated layer.
3920
- onLayerSelected = () => {
3921
- if (this.selected) this.select(null, null);
4120
+ onLayerSelected = ({ layerId }) => {
4121
+ if (layerId && this.selected) this.select(null, null);
3922
4122
  };
3923
4123
  onObjectModified = (event) => {
3924
4124
  const object = event.target;
@@ -3958,7 +4158,7 @@ var LayerMaskManager = class extends MaskStackStore {
3958
4158
  if (index === -1) return false;
3959
4159
  this.endEdit(false);
3960
4160
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
3961
- const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
4161
+ const group = host.clipPath instanceof import_fabric19.Group ? host.clipPath : null;
3962
4162
  if (!group) return false;
3963
4163
  const children = group.getObjects();
3964
4164
  const child = children[children.length - entries.length + index];
@@ -3978,6 +4178,7 @@ var LayerMaskManager = class extends MaskStackStore {
3978
4178
  }
3979
4179
  select(target, maskId) {
3980
4180
  this.selected = target && maskId ? { target, maskId } : null;
4181
+ if (this.selected) this.layers.select(null);
3981
4182
  this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
3982
4183
  }
3983
4184
  // ─── Mutations ───────────────────────────────────────
@@ -4264,7 +4465,7 @@ var CanvasEditor = class {
4264
4465
  const widthPx = this.units.toPixels(config.width);
4265
4466
  const heightPx = this.units.toPixels(config.height);
4266
4467
  this.designBackground = config.backgroundColor ?? "#ffffff";
4267
- this.canvas = new import_fabric19.Canvas(canvasElement, {
4468
+ this.canvas = new import_fabric20.Canvas(canvasElement, {
4268
4469
  width: widthPx,
4269
4470
  height: heightPx,
4270
4471
  backgroundColor: this.designBackground,
@@ -4298,7 +4499,7 @@ var CanvasEditor = class {
4298
4499
  // ─── Layer Operations ────────────────────────────────
4299
4500
  async addImage(url, options) {
4300
4501
  try {
4301
- const img = await import_fabric19.FabricImage.fromURL(
4502
+ const img = await import_fabric20.FabricImage.fromURL(
4302
4503
  url,
4303
4504
  {},
4304
4505
  { originX: "left", originY: "top", ...options }
@@ -4324,7 +4525,7 @@ var CanvasEditor = class {
4324
4525
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4325
4526
  const previous = layer.fabricObject;
4326
4527
  try {
4327
- const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4528
+ const replacement = await import_fabric20.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4328
4529
  replacement.set({
4329
4530
  left: previous.left,
4330
4531
  top: previous.top,
@@ -4353,7 +4554,7 @@ var CanvasEditor = class {
4353
4554
  }
4354
4555
  }
4355
4556
  addText(text, options) {
4356
- const textbox = new import_fabric19.Textbox(text, {
4557
+ const textbox = new import_fabric20.Textbox(text, {
4357
4558
  fontSize: 32,
4358
4559
  fontFamily: "Arial",
4359
4560
  fill: "#000000",
@@ -4484,10 +4685,10 @@ var CanvasEditor = class {
4484
4685
  const next = { ...previous, ...adjustments };
4485
4686
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4486
4687
  image.filters = [
4487
- new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4488
- new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4489
- new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4490
- new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4688
+ new import_fabric20.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4689
+ new import_fabric20.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4690
+ new import_fabric20.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4691
+ new import_fabric20.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4491
4692
  ];
4492
4693
  layer.meta.imageAdjustments = next;
4493
4694
  image.applyFilters();
@@ -4528,10 +4729,9 @@ var CanvasEditor = class {
4528
4729
  this.layers.detach(layer.id);
4529
4730
  }
4530
4731
  }
4531
- const group = new import_fabric19.Group(objects);
4732
+ const group = new import_fabric20.Group(objects);
4532
4733
  const grouped = this.layers.add("group", group, name);
4533
- for (const child of children) child.parentId = grouped.id;
4534
- grouped.children = children;
4734
+ this.layers.adoptChildren(grouped, children);
4535
4735
  this.layers.reorder(grouped.id, insertIndex);
4536
4736
  this.layers.select(grouped.id);
4537
4737
  this.history.save();
@@ -4591,7 +4791,7 @@ var CanvasEditor = class {
4591
4791
  const layer = this.layers.get(id);
4592
4792
  if (!layer) throw new Error(`Layer not found: ${id}`);
4593
4793
  try {
4594
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
4794
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric20.FabricImage) {
4595
4795
  const image = await layer.fabricObject.clone();
4596
4796
  image.set({
4597
4797
  left: 0,
@@ -4867,7 +5067,7 @@ var CanvasEditor = class {
4867
5067
  return;
4868
5068
  }
4869
5069
  try {
4870
- const image = await import_fabric19.FabricImage.fromURL(
5070
+ const image = await import_fabric20.FabricImage.fromURL(
4871
5071
  url,
4872
5072
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
4873
5073
  { originX: "left", originY: "top" }
@@ -5140,6 +5340,7 @@ var CanvasEditor = class {
5140
5340
  this.crop.dispose();
5141
5341
  this.history.dispose();
5142
5342
  this.patterns.dispose();
5343
+ this.curves.dispose();
5143
5344
  this.events.removeAllListeners();
5144
5345
  this.canvas.dispose();
5145
5346
  }
@@ -5199,11 +5400,15 @@ var DEFAULT_PATTERN_CONFIG = {
5199
5400
  horizontalSpacing: 0,
5200
5401
  verticalSpacing: 0,
5201
5402
  angle: 0,
5202
- horizontalOffset: 0,
5403
+ // Half a tile: the offset only bites in the brick modes, and a brick course
5404
+ // that is not half-shifted is just a grid with extra steps.
5405
+ horizontalOffset: 50,
5203
5406
  offsetX: 0,
5204
5407
  offsetY: 0,
5205
5408
  rotationStepH: 0,
5206
- rotationStepV: 0
5409
+ rotationStepV: 0,
5410
+ horizontalLimit: 0,
5411
+ verticalLimit: 0
5207
5412
  };
5208
5413
 
5209
5414
  // src/presets.ts
@@ -5309,6 +5514,7 @@ var AnnotationOverlay = class {
5309
5514
  exportPNG,
5310
5515
  exportPrintArea,
5311
5516
  exportSVG,
5517
+ fitTextWidth,
5312
5518
  fitToBox,
5313
5519
  generateId,
5314
5520
  isCssColor,