@overtone-art/canvas-editor-core 0.6.2 → 0.6.4

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 {
@@ -1225,10 +1226,63 @@ var CropController = class {
1225
1226
  var STROKE2 = "#22c55e";
1226
1227
 
1227
1228
  // src/pattern/pattern-manager.ts
1228
- var import_fabric5 = require("fabric");
1229
+ var import_fabric6 = require("fabric");
1229
1230
 
1230
1231
  // src/pattern/tiled-pattern-object.ts
1232
+ var import_fabric5 = require("fabric");
1233
+
1234
+ // src/text-fit.ts
1231
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
+ }
1232
1286
 
1233
1287
  // src/pattern/tile-geometry.ts
1234
1288
  var MAX_TILES_PER_AXIS = 200;
@@ -1241,13 +1295,13 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
1241
1295
  const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
1242
1296
  const cols = Math.ceil(span / tileW) + 2;
1243
1297
  const rows = Math.ceil(span / tileH) + 2;
1244
- const halfCols = Math.ceil(cols / 2);
1245
- 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));
1246
1300
  const shiftX = tileW * (clampOffset(config.offsetX) / 100);
1247
1301
  const shiftY = tileH * (clampOffset(config.offsetY) / 100);
1248
1302
  const placements = [];
1249
- for (let j = -halfRows; j <= halfRows; j++) {
1250
- for (let i = -halfCols; i <= halfCols; i++) {
1303
+ for (let j = rowFrom; j <= rowTo; j++) {
1304
+ for (let i = colFrom; i <= colTo; i++) {
1251
1305
  let x = i * tileW + shiftX;
1252
1306
  let y = j * tileH + shiftY;
1253
1307
  if (config.mode === "brick-horizontal" && mod2(j) === 1) {
@@ -1277,6 +1331,12 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, dra
1277
1331
  }
1278
1332
  ctx.restore();
1279
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
+ }
1280
1340
  function cornerRadius(origin, w, h) {
1281
1341
  const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
1282
1342
  const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
@@ -1295,7 +1355,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
1295
1355
  var MAX_SNAPSHOT_SCALE = 8;
1296
1356
  var SNAPSHOT_SHRINK_FACTOR = 2;
1297
1357
  var TILE_BLEED_DEVICE_PX = 2;
1298
- var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
1358
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.FabricObject {
1299
1359
  static type = "TiledPattern";
1300
1360
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
1301
1361
  source;
@@ -1348,7 +1408,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1348
1408
  */
1349
1409
  syncBox() {
1350
1410
  const { tileW, tileH } = this.baseTile();
1351
- const origin = this.source.getCenterPoint();
1411
+ const origin = this.source.getCenterPoint().add(this.inkOffset());
1352
1412
  const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
1353
1413
  this.set({
1354
1414
  left: anchor.x,
@@ -1378,7 +1438,8 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1378
1438
  const { tileW, tileH } = this.liveTile();
1379
1439
  const centre = this.getCenterPoint();
1380
1440
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1381
- 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);
1382
1443
  }
1383
1444
  _render(ctx) {
1384
1445
  const { width, height } = this.area;
@@ -1393,8 +1454,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1393
1454
  ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
1394
1455
  ctx.rotate(-angle * Math.PI / 180);
1395
1456
  ctx.translate(-centre.x, -centre.y);
1396
- const shift = this.shiftVector(tileW, tileH, angle);
1397
- const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1457
+ const origin = { x: centre.x, y: centre.y };
1398
1458
  const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1399
1459
  drawTiles(
1400
1460
  ctx,
@@ -1423,7 +1483,6 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1423
1483
  if (!snapshot) return [];
1424
1484
  const centre = this.getCenterPoint();
1425
1485
  const angle = this.liveAngle();
1426
- const shift = this.shiftVector(tileW, tileH, angle);
1427
1486
  drawTiles(
1428
1487
  ctx,
1429
1488
  snapshot,
@@ -1432,7 +1491,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1432
1491
  height,
1433
1492
  tileW,
1434
1493
  tileH,
1435
- { x: centre.x - shift.x, y: centre.y - shift.y },
1494
+ { x: centre.x, y: centre.y },
1436
1495
  // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1437
1496
  this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1438
1497
  );
@@ -1475,7 +1534,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1475
1534
  return Promise.resolve(copy);
1476
1535
  }
1477
1536
  /**
1478
- * 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
1479
1538
  * room for but never paints.
1480
1539
  *
1481
1540
  * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
@@ -1483,11 +1542,18 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1483
1542
  * stroke. Stepping the grid by that box puts a transparent seam between every
1484
1543
  * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1485
1544
  * box it is stepped by.
1545
+ *
1546
+ * Text is the same problem an order of magnitude larger — see {@link textInk}.
1486
1547
  */
1487
1548
  sourceBox() {
1549
+ const text = asText(this.source);
1550
+ const paints = paintsStroke(this.source);
1488
1551
  const rect = this.source.getBoundingRect();
1489
- if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1490
- 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);
1491
1557
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1492
1558
  const cos = Math.abs(Math.cos(radians));
1493
1559
  const sin = Math.abs(Math.sin(radians));
@@ -1496,6 +1562,22 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1496
1562
  height: dims.x * sin + dims.y * cos
1497
1563
  };
1498
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
+ }
1499
1581
  /** The source's on-canvas width, before the tile scale. */
1500
1582
  baseW() {
1501
1583
  return Math.max(1, this.sourceBox().width);
@@ -1555,11 +1637,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1555
1637
  const radians = angle * Math.PI / 180;
1556
1638
  const cos = Math.cos(radians);
1557
1639
  const sin = Math.sin(radians);
1558
- 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);
1559
1641
  }
1560
1642
  anchorFromOrigin(origin, tileW, tileH, angle) {
1561
1643
  const shift = this.shiftVector(tileW, tileH, angle);
1562
- 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);
1563
1645
  }
1564
1646
  /**
1565
1647
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1572,7 +1654,15 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1572
1654
  const source = this.source;
1573
1655
  const opacity = source.opacity;
1574
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;
1575
1661
  try {
1662
+ if (hug && text) {
1663
+ text.set({ width: fitted });
1664
+ text.initDimensions?.();
1665
+ }
1576
1666
  const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
1577
1667
  if (!el.width || !el.height) return null;
1578
1668
  this.snapshotEl = el;
@@ -1581,6 +1671,10 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric4.Fabric
1581
1671
  return this.snapshotEl;
1582
1672
  } finally {
1583
1673
  source.opacity = opacity;
1674
+ if (hug && text && authoredWidth !== void 0) {
1675
+ text.set({ width: authoredWidth });
1676
+ text.initDimensions?.();
1677
+ }
1584
1678
  source.dirty = false;
1585
1679
  }
1586
1680
  return this.snapshotEl;
@@ -1819,7 +1913,7 @@ async function unbakeLegacyLayer(layer, state) {
1819
1913
  cropY: state.original.cropY,
1820
1914
  angle: state.original.angle
1821
1915
  });
1822
- 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;
1823
1917
  restoreLocks(image, state.originalLocks);
1824
1918
  image.setCoords();
1825
1919
  }
@@ -1837,7 +1931,7 @@ function restoreLocks(obj, locks) {
1837
1931
  }
1838
1932
 
1839
1933
  // src/text-curve.ts
1840
- var import_fabric6 = require("fabric");
1934
+ var import_fabric7 = require("fabric");
1841
1935
 
1842
1936
  // src/text-curve-geometry.ts
1843
1937
  var DEFAULT_TEXT_CURVE = {
@@ -1867,7 +1961,11 @@ function normalizeTextCurve(config) {
1867
1961
  shape: config.shape ?? inferred,
1868
1962
  arc,
1869
1963
  wave,
1870
- waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
1964
+ waveLength: clamp(
1965
+ config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
1966
+ MIN_WAVE_LENGTH,
1967
+ MAX_WAVE_LENGTH
1968
+ ),
1871
1969
  offset: clamp(config.offset ?? 0, -100, 100),
1872
1970
  centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
1873
1971
  };
@@ -1957,18 +2055,55 @@ function buildCurvePathData(config, width, fontSize) {
1957
2055
  }
1958
2056
 
1959
2057
  // src/text-curve.ts
1960
- var MEASURE_WIDTH = 1e5;
2058
+ var MEASURE_WIDTH2 = 1e5;
1961
2059
  var PATH_SLACK = 0.06;
1962
2060
  var FALLBACK_LINE_HEIGHT = 1.16;
1963
2061
  var patches = /* @__PURE__ */ new WeakMap();
2062
+ var dimensionPatches = /* @__PURE__ */ new WeakMap();
1964
2063
  var authoredCaching = /* @__PURE__ */ new WeakMap();
2064
+ var GLYPH_HALF_HEIGHT = 0.7;
2065
+ function curvedInkSize(text) {
2066
+ const lineCount = text._textLines?.length ?? 0;
2067
+ for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
2068
+ const lines = text.__charBounds;
2069
+ if (!lines?.length) return null;
2070
+ let halfWidth = 0;
2071
+ let halfHeight = 0;
2072
+ for (const line of lines) {
2073
+ for (const box of line ?? []) {
2074
+ const { renderLeft, renderTop } = box;
2075
+ if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
2076
+ const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
2077
+ const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
2078
+ const angle = box.angle ?? 0;
2079
+ const cos = Math.abs(Math.cos(angle));
2080
+ const sin = Math.abs(Math.sin(angle));
2081
+ halfWidth = Math.max(
2082
+ halfWidth,
2083
+ Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
2084
+ );
2085
+ halfHeight = Math.max(
2086
+ halfHeight,
2087
+ Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
2088
+ );
2089
+ }
2090
+ }
2091
+ if (halfWidth === 0 && halfHeight === 0) return null;
2092
+ return { width: halfWidth * 2, height: halfHeight * 2 };
2093
+ }
2094
+ function growBoxToInk(text) {
2095
+ const ink = curvedInkSize(text);
2096
+ if (!ink) return;
2097
+ if (ink.width > (text.width ?? 0)) text.width = ink.width;
2098
+ if (ink.height > (text.height ?? 0)) text.height = ink.height;
2099
+ }
1965
2100
  function isCurvable(object) {
1966
2101
  return !!object && typeof object.text === "string";
1967
2102
  }
1968
2103
  function measureText(text) {
1969
2104
  const authored = text.width;
1970
2105
  try {
1971
- text.set({ width: MEASURE_WIDTH });
2106
+ text.set({ width: MEASURE_WIDTH2 });
1972
2107
  text.initDimensions?.();
1973
2108
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1974
2109
  } finally {
@@ -1985,7 +2120,7 @@ function lineHeightOf(text) {
1985
2120
  return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
1986
2121
  }
1987
2122
  function toFabricPath(curve) {
1988
- return new import_fabric6.Path(curve.data, { visible: false, objectCaching: false });
2123
+ return new import_fabric7.Path(curve.data, { visible: false, objectCaching: false });
1989
2124
  }
1990
2125
  var TextCurveManager = class {
1991
2126
  constructor(canvas, layers, history, events) {
@@ -2113,6 +2248,35 @@ var TextCurveManager = class {
2113
2248
  pathStartOffset: 0
2114
2249
  });
2115
2250
  this.patchLineMeasure(text, paths, union);
2251
+ this.patchDimensions(text);
2252
+ }
2253
+ /**
2254
+ * Re-apply {@link growBoxToInk} after every one of fabric's own dimension
2255
+ * passes. Typing, a font change, a style edit and a state restore all call
2256
+ * `initDimensions`, and each one puts the box back to the path's bounding box
2257
+ * — so growing it once, here, would hold only until the next keystroke.
2258
+ */
2259
+ patchDimensions(text) {
2260
+ if (dimensionPatches.has(text)) return;
2261
+ const original = text.initDimensions;
2262
+ if (typeof original !== "function") return;
2263
+ const installed = function() {
2264
+ original.call(this);
2265
+ growBoxToInk(this);
2266
+ };
2267
+ const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "initDimensions");
2268
+ text.initDimensions = installed;
2269
+ dimensionPatches.set(text, { original, installed, ownedOriginal });
2270
+ }
2271
+ /** Restore fabric's own dimension pass, if this manager replaced it. */
2272
+ unpatchDimensions(text) {
2273
+ const patch = dimensionPatches.get(text);
2274
+ if (!patch) return;
2275
+ if (text.initDimensions === patch.installed) {
2276
+ if (patch.ownedOriginal) text.initDimensions = patch.original;
2277
+ else delete text.initDimensions;
2278
+ }
2279
+ dimensionPatches.delete(text);
2116
2280
  }
2117
2281
  /**
2118
2282
  * Fabric lays every line of a text object along `this.path`, from one
@@ -2164,6 +2328,7 @@ var TextCurveManager = class {
2164
2328
  }
2165
2329
  detach(text, authoredWidth) {
2166
2330
  this.unpatchLineMeasure(text);
2331
+ this.unpatchDimensions(text);
2167
2332
  const caching = authoredCaching.get(text);
2168
2333
  if (caching !== void 0) {
2169
2334
  text.set({ objectCaching: caching });
@@ -2175,7 +2340,7 @@ var TextCurveManager = class {
2175
2340
  };
2176
2341
 
2177
2342
  // src/mask-presets/manager.ts
2178
- var import_fabric7 = require("fabric");
2343
+ var import_fabric8 = require("fabric");
2179
2344
 
2180
2345
  // src/mask-presets/shapes.ts
2181
2346
  var SHAPE_MASK_IDS = [
@@ -2442,13 +2607,13 @@ var MaskPresetManager = class {
2442
2607
  objectCaching: false
2443
2608
  };
2444
2609
  if (isShapeMaskId(id)) {
2445
- return new import_fabric7.Path(shapeMaskPathData(id), {
2610
+ return new import_fabric8.Path(shapeMaskPathData(id), {
2446
2611
  ...shared,
2447
2612
  scaleX: width / SHAPE_MASK_BOX,
2448
2613
  scaleY: height / SHAPE_MASK_BOX
2449
2614
  });
2450
2615
  }
2451
- return new import_fabric7.FabricImage(renderTextureMask(id), {
2616
+ return new import_fabric8.FabricImage(renderTextureMask(id), {
2452
2617
  ...shared,
2453
2618
  scaleX: width / TEXTURE_MASK_SIZE,
2454
2619
  scaleY: height / TEXTURE_MASK_SIZE
@@ -2457,7 +2622,7 @@ var MaskPresetManager = class {
2457
2622
  };
2458
2623
 
2459
2624
  // src/shadow.ts
2460
- var import_fabric8 = require("fabric");
2625
+ var import_fabric9 = require("fabric");
2461
2626
 
2462
2627
  // src/utils/color.ts
2463
2628
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -2495,7 +2660,7 @@ function applyLayerShadow(object, config) {
2495
2660
  }
2496
2661
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2497
2662
  object.set({
2498
- shadow: new import_fabric8.Shadow({
2663
+ shadow: new import_fabric9.Shadow({
2499
2664
  color,
2500
2665
  blur: Math.max(0, next.blur),
2501
2666
  offsetX: next.offsetX,
@@ -2609,7 +2774,7 @@ var UnitConverter = class {
2609
2774
  };
2610
2775
 
2611
2776
  // src/serialization.ts
2612
- var import_fabric9 = require("fabric");
2777
+ var import_fabric10 = require("fabric");
2613
2778
  var VERSION = "2.0.0";
2614
2779
  function serializeEditor(editor) {
2615
2780
  return {
@@ -2645,7 +2810,7 @@ async function deserializeEditor(editor, state) {
2645
2810
  }
2646
2811
  const staged = await Promise.all(
2647
2812
  state.layers.map(async (serialized) => {
2648
- const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
2813
+ const fabricObject = (await import_fabric10.util.enlivenObjects([serialized.fabricObject]))[0];
2649
2814
  if (!fabricObject) {
2650
2815
  const source = serialized.fabricObject.src;
2651
2816
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2656,7 +2821,7 @@ async function deserializeEditor(editor, state) {
2656
2821
  return { serialized, fabricObject };
2657
2822
  })
2658
2823
  );
2659
- const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
2824
+ const stagedBackground = state.backgroundImage ? (await import_fabric10.util.enlivenObjects([state.backgroundImage]))[0] : null;
2660
2825
  if (state.backgroundImage && !stagedBackground) {
2661
2826
  const source = state.backgroundImage.src;
2662
2827
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2708,7 +2873,7 @@ function restoreLayer(editor, serialized, fabricObject) {
2708
2873
  }
2709
2874
 
2710
2875
  // src/export.ts
2711
- var import_fabric10 = require("fabric");
2876
+ var import_fabric11 = require("fabric");
2712
2877
 
2713
2878
  // src/displacement.ts
2714
2879
  var CHANNEL_INDEX = {
@@ -2802,7 +2967,7 @@ async function exportPNG(canvas, options = {}) {
2802
2967
  }
2803
2968
  async function exportIsolatedPNG(source, objects, options = {}) {
2804
2969
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2805
- const canvas = new import_fabric10.StaticCanvas(element, {
2970
+ const canvas = new import_fabric11.StaticCanvas(element, {
2806
2971
  width: options.width ?? source.getWidth(),
2807
2972
  height: options.height ?? source.getHeight(),
2808
2973
  backgroundColor: options.backgroundColor || void 0
@@ -2832,7 +2997,7 @@ async function exportPrintArea(source, area, options = {}) {
2832
2997
  throw new Error("Print area does not overlap the canvas");
2833
2998
  }
2834
2999
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2835
- const canvas = new import_fabric10.StaticCanvas(element, { width, height });
3000
+ const canvas = new import_fabric11.StaticCanvas(element, { width, height });
2836
3001
  try {
2837
3002
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2838
3003
  if (clones.length) canvas.add(...clones);
@@ -3272,7 +3437,7 @@ var ProjectManager = class {
3272
3437
  };
3273
3438
 
3274
3439
  // src/mask.ts
3275
- var import_fabric11 = require("fabric");
3440
+ var import_fabric12 = require("fabric");
3276
3441
  var MaskRefinementError = class extends Error {
3277
3442
  constructor(code, message, cause) {
3278
3443
  super(message);
@@ -3304,7 +3469,7 @@ var MaskController = class {
3304
3469
  throw new Error("Mask dimensions must be positive integers");
3305
3470
  }
3306
3471
  const backing = this.makeCanvas(width, height);
3307
- const image = new import_fabric11.FabricImage(backing, {
3472
+ const image = new import_fabric12.FabricImage(backing, {
3308
3473
  left: 0,
3309
3474
  top: 0,
3310
3475
  originX: "left",
@@ -3530,10 +3695,10 @@ var MaskController = class {
3530
3695
  };
3531
3696
 
3532
3697
  // src/masks/manager.ts
3533
- var import_fabric18 = require("fabric");
3698
+ var import_fabric19 = require("fabric");
3534
3699
 
3535
3700
  // src/masks/compose.ts
3536
- var import_fabric12 = require("fabric");
3701
+ var import_fabric13 = require("fabric");
3537
3702
  var MODE_OPERATION = {
3538
3703
  add: "source-over",
3539
3704
  subtract: "destination-out",
@@ -3543,7 +3708,7 @@ function neutralize(child) {
3543
3708
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3544
3709
  }
3545
3710
  function baseRect(box) {
3546
- return new import_fabric12.Rect({
3711
+ return new import_fabric13.Rect({
3547
3712
  left: box.left,
3548
3713
  top: box.top,
3549
3714
  width: Math.max(1, box.width),
@@ -3570,7 +3735,7 @@ function composeMaskGroup(children, entries, options) {
3570
3735
  });
3571
3736
  const first = entries.find((entry) => entry.visible);
3572
3737
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3573
- return new import_fabric12.Group(withBase, {
3738
+ return new import_fabric13.Group(withBase, {
3574
3739
  absolutePositioned: options.absolute,
3575
3740
  // Cached, so the children's compositing operations resolve against each
3576
3741
  // other instead of against the page underneath the mask.
@@ -3584,15 +3749,15 @@ function needsAbsoluteSpace(entries) {
3584
3749
  }
3585
3750
 
3586
3751
  // src/masks/edit.ts
3587
- var import_fabric14 = require("fabric");
3752
+ var import_fabric15 = require("fabric");
3588
3753
 
3589
3754
  // src/masks/space.ts
3590
- var import_fabric13 = require("fabric");
3755
+ var import_fabric14 = require("fabric");
3591
3756
  function matrixOf(object) {
3592
3757
  return object.calcTransformMatrix();
3593
3758
  }
3594
3759
  function applyMatrix(object, matrix) {
3595
- const decomposed = import_fabric13.util.qrDecompose(matrix);
3760
+ const decomposed = import_fabric14.util.qrDecompose(matrix);
3596
3761
  object.set({
3597
3762
  flipX: false,
3598
3763
  flipY: false,
@@ -3609,19 +3774,19 @@ function applyMatrix(object, matrix) {
3609
3774
  object.setCoords();
3610
3775
  }
3611
3776
  function toCanvasSpace(object, host) {
3612
- applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3777
+ applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3613
3778
  }
3614
3779
  function toHostSpace(object, host) {
3615
3780
  applyMatrix(
3616
3781
  object,
3617
- import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
3782
+ import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object))
3618
3783
  );
3619
3784
  }
3620
3785
  function relativeMatrix(object, host) {
3621
- return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
3786
+ return import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object));
3622
3787
  }
3623
3788
  function applyRelativeMatrix(object, host, rel) {
3624
- applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
3789
+ applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), rel));
3625
3790
  }
3626
3791
  function asObject(clip) {
3627
3792
  return clip;
@@ -3734,8 +3899,8 @@ var MaskEditController = class {
3734
3899
  if (!this.handle || !this.child || !this.group) return;
3735
3900
  applyMatrix(
3736
3901
  this.child,
3737
- import_fabric14.util.multiplyTransformMatrices(
3738
- import_fabric14.util.invertTransform(matrixOf(this.group)),
3902
+ import_fabric15.util.multiplyTransformMatrices(
3903
+ import_fabric15.util.invertTransform(matrixOf(this.group)),
3739
3904
  matrixOf(this.handle)
3740
3905
  )
3741
3906
  );
@@ -3746,15 +3911,15 @@ var MaskEditController = class {
3746
3911
  };
3747
3912
 
3748
3913
  // src/masks/store.ts
3749
- var import_fabric17 = require("fabric");
3914
+ var import_fabric18 = require("fabric");
3750
3915
 
3751
3916
  // src/masks/host.ts
3752
- var import_fabric15 = require("fabric");
3917
+ var import_fabric16 = require("fabric");
3753
3918
  function findCanvasHost(layers) {
3754
3919
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3755
3920
  }
3756
3921
  function createCanvasHost(canvas, layers) {
3757
- const rect = new import_fabric15.Rect({
3922
+ const rect = new import_fabric16.Rect({
3758
3923
  left: 0,
3759
3924
  top: 0,
3760
3925
  width: canvas.getWidth(),
@@ -3803,9 +3968,9 @@ function hostBoxOf(canvas, host, absolute) {
3803
3968
  }
3804
3969
 
3805
3970
  // src/masks/install.ts
3806
- var import_fabric16 = require("fabric");
3971
+ var import_fabric17 = require("fabric");
3807
3972
  function convertSpace(host, sources, absolute) {
3808
- const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
3973
+ const wasAbsolute = host.clipPath instanceof import_fabric17.Group ? host.clipPath.absolutePositioned : absolute;
3809
3974
  if (absolute === wasAbsolute) return;
3810
3975
  for (const source of sources) {
3811
3976
  if (absolute) toCanvasSpace(source, host);
@@ -3897,7 +4062,7 @@ var MaskStackStore = class {
3897
4062
  const clip = host.clipPath;
3898
4063
  if (!clip) return [];
3899
4064
  const entries = this.list(target);
3900
- if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
4065
+ if (entries.length === 0 || !(clip instanceof import_fabric18.Group)) return [asObject(clip)];
3901
4066
  const children = unwrapGroup(clip);
3902
4067
  const extra = children.length - entries.length;
3903
4068
  return extra > 0 ? children.slice(extra) : children;
@@ -3956,8 +4121,8 @@ var LayerMaskManager = class extends MaskStackStore {
3956
4121
  onLayersChanged = () => this.pin();
3957
4122
  // Selecting a layer means the user has moved on from the mask they had open;
3958
4123
  // leaving both selected would show mask controls for an unrelated layer.
3959
- onLayerSelected = () => {
3960
- if (this.selected) this.select(null, null);
4124
+ onLayerSelected = ({ layerId }) => {
4125
+ if (layerId && this.selected) this.select(null, null);
3961
4126
  };
3962
4127
  onObjectModified = (event) => {
3963
4128
  const object = event.target;
@@ -3997,7 +4162,7 @@ var LayerMaskManager = class extends MaskStackStore {
3997
4162
  if (index === -1) return false;
3998
4163
  this.endEdit(false);
3999
4164
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4000
- const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
4165
+ const group = host.clipPath instanceof import_fabric19.Group ? host.clipPath : null;
4001
4166
  if (!group) return false;
4002
4167
  const children = group.getObjects();
4003
4168
  const child = children[children.length - entries.length + index];
@@ -4017,6 +4182,7 @@ var LayerMaskManager = class extends MaskStackStore {
4017
4182
  }
4018
4183
  select(target, maskId) {
4019
4184
  this.selected = target && maskId ? { target, maskId } : null;
4185
+ if (this.selected) this.layers.select(null);
4020
4186
  this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
4021
4187
  }
4022
4188
  // ─── Mutations ───────────────────────────────────────
@@ -4177,6 +4343,9 @@ var LayerMaskManager = class extends MaskStackStore {
4177
4343
  if (resolved === layerId) return null;
4178
4344
  const object = layer.fabricObject;
4179
4345
  if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
4346
+ const stackHost = this.host(resolved);
4347
+ const absolute = resolved === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(resolved));
4348
+ if (!absolute && stackHost) toHostSpace(object, stackHost);
4180
4349
  this.history.beginTransaction();
4181
4350
  try {
4182
4351
  if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
@@ -4303,7 +4472,7 @@ var CanvasEditor = class {
4303
4472
  const widthPx = this.units.toPixels(config.width);
4304
4473
  const heightPx = this.units.toPixels(config.height);
4305
4474
  this.designBackground = config.backgroundColor ?? "#ffffff";
4306
- this.canvas = new import_fabric19.Canvas(canvasElement, {
4475
+ this.canvas = new import_fabric20.Canvas(canvasElement, {
4307
4476
  width: widthPx,
4308
4477
  height: heightPx,
4309
4478
  backgroundColor: this.designBackground,
@@ -4337,7 +4506,7 @@ var CanvasEditor = class {
4337
4506
  // ─── Layer Operations ────────────────────────────────
4338
4507
  async addImage(url, options) {
4339
4508
  try {
4340
- const img = await import_fabric19.FabricImage.fromURL(
4509
+ const img = await import_fabric20.FabricImage.fromURL(
4341
4510
  url,
4342
4511
  {},
4343
4512
  { originX: "left", originY: "top", ...options }
@@ -4363,7 +4532,7 @@ var CanvasEditor = class {
4363
4532
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4364
4533
  const previous = layer.fabricObject;
4365
4534
  try {
4366
- const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4535
+ const replacement = await import_fabric20.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4367
4536
  replacement.set({
4368
4537
  left: previous.left,
4369
4538
  top: previous.top,
@@ -4392,7 +4561,7 @@ var CanvasEditor = class {
4392
4561
  }
4393
4562
  }
4394
4563
  addText(text, options) {
4395
- const textbox = new import_fabric19.Textbox(text, {
4564
+ const textbox = new import_fabric20.Textbox(text, {
4396
4565
  fontSize: 32,
4397
4566
  fontFamily: "Arial",
4398
4567
  fill: "#000000",
@@ -4523,10 +4692,10 @@ var CanvasEditor = class {
4523
4692
  const next = { ...previous, ...adjustments };
4524
4693
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4525
4694
  image.filters = [
4526
- new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4527
- new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4528
- new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4529
- new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4695
+ new import_fabric20.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4696
+ new import_fabric20.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4697
+ new import_fabric20.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4698
+ new import_fabric20.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4530
4699
  ];
4531
4700
  layer.meta.imageAdjustments = next;
4532
4701
  image.applyFilters();
@@ -4567,7 +4736,7 @@ var CanvasEditor = class {
4567
4736
  this.layers.detach(layer.id);
4568
4737
  }
4569
4738
  }
4570
- const group = new import_fabric19.Group(objects);
4739
+ const group = new import_fabric20.Group(objects);
4571
4740
  const grouped = this.layers.add("group", group, name);
4572
4741
  this.layers.adoptChildren(grouped, children);
4573
4742
  this.layers.reorder(grouped.id, insertIndex);
@@ -4629,7 +4798,7 @@ var CanvasEditor = class {
4629
4798
  const layer = this.layers.get(id);
4630
4799
  if (!layer) throw new Error(`Layer not found: ${id}`);
4631
4800
  try {
4632
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
4801
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric20.FabricImage) {
4633
4802
  const image = await layer.fabricObject.clone();
4634
4803
  image.set({
4635
4804
  left: 0,
@@ -4905,7 +5074,7 @@ var CanvasEditor = class {
4905
5074
  return;
4906
5075
  }
4907
5076
  try {
4908
- const image = await import_fabric19.FabricImage.fromURL(
5077
+ const image = await import_fabric20.FabricImage.fromURL(
4909
5078
  url,
4910
5079
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
4911
5080
  { originX: "left", originY: "top" }
@@ -5238,11 +5407,15 @@ var DEFAULT_PATTERN_CONFIG = {
5238
5407
  horizontalSpacing: 0,
5239
5408
  verticalSpacing: 0,
5240
5409
  angle: 0,
5241
- horizontalOffset: 0,
5410
+ // Half a tile: the offset only bites in the brick modes, and a brick course
5411
+ // that is not half-shifted is just a grid with extra steps.
5412
+ horizontalOffset: 50,
5242
5413
  offsetX: 0,
5243
5414
  offsetY: 0,
5244
5415
  rotationStepH: 0,
5245
- rotationStepV: 0
5416
+ rotationStepV: 0,
5417
+ horizontalLimit: 0,
5418
+ verticalLimit: 0
5246
5419
  };
5247
5420
 
5248
5421
  // src/presets.ts
@@ -5348,6 +5521,7 @@ var AnnotationOverlay = class {
5348
5521
  exportPNG,
5349
5522
  exportPrintArea,
5350
5523
  exportSVG,
5524
+ fitTextWidth,
5351
5525
  fitToBox,
5352
5526
  generateId,
5353
5527
  isCssColor,