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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,12 +47,14 @@ __export(index_exports, {
47
47
  TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
48
48
  TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
49
49
  TextCurveManager: () => TextCurveManager,
50
+ TextWrapManager: () => TextWrapManager,
50
51
  TiledPatternObject: () => TiledPatternObject,
51
52
  UnitConverter: () => UnitConverter,
52
53
  applyAspectLock: () => applyAspectLock,
53
54
  applyLayerShadow: () => applyLayerShadow,
54
55
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
55
56
  applySelectionStyle: () => applySelectionStyle,
57
+ applyTextWrapToObject: () => applyTextWrapToObject,
56
58
  buildCurveLinePaths: () => buildCurveLinePaths,
57
59
  buildCurvePathData: () => buildCurvePathData,
58
60
  clamp: () => clamp,
@@ -80,7 +82,9 @@ __export(index_exports, {
80
82
  isTextureMaskId: () => isTextureMaskId,
81
83
  needsAbsoluteSpace: () => needsAbsoluteSpace,
82
84
  normalizeTextCurve: () => normalizeTextCurve,
85
+ preWrapWordSplit: () => preWrapWordSplit,
83
86
  readLayerShadow: () => readLayerShadow,
87
+ readTextWrap: () => readTextWrap,
84
88
  renderTextureMask: () => renderTextureMask,
85
89
  resetTransform: () => resetTransform,
86
90
  restoreLocks: () => restoreLocks,
@@ -97,7 +101,7 @@ __export(index_exports, {
97
101
  module.exports = __toCommonJS(index_exports);
98
102
 
99
103
  // src/editor.ts
100
- var import_fabric20 = require("fabric");
104
+ var import_fabric21 = require("fabric");
101
105
 
102
106
  // src/events.ts
103
107
  var EventEmitter = class {
@@ -1235,6 +1239,9 @@ var import_fabric5 = require("fabric");
1235
1239
  var import_fabric4 = require("fabric");
1236
1240
  var TEXT_FIT_SLACK = 0.5;
1237
1241
  var MEASURE_WIDTH = 1e5;
1242
+ var GROWTH_FACTOR = 1.1;
1243
+ var GROWTH_TRIES = 8;
1244
+ var SEARCH_TRIES = 24;
1238
1245
  function unwrappedWidth(text) {
1239
1246
  const authored = text.width;
1240
1247
  try {
@@ -1262,26 +1269,74 @@ function textInk(text) {
1262
1269
  const flip = text.flipX ? -1 : 1;
1263
1270
  return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1264
1271
  }
1272
+ function wrapsAt(text, width) {
1273
+ text.set({ width });
1274
+ text.initDimensions?.();
1275
+ const authored = text.text.split("\n").length;
1276
+ return (text._textLines?.length ?? 0) > authored;
1277
+ }
1278
+ function widenPastSoftWrap(text, fitted) {
1279
+ if (!wrapsAt(text, fitted)) return fitted;
1280
+ let low = fitted;
1281
+ let high = fitted;
1282
+ let bracketed = false;
1283
+ for (let tries = 0; tries < GROWTH_TRIES; tries += 1) {
1284
+ low = high;
1285
+ high = high * GROWTH_FACTOR + 1;
1286
+ if (!wrapsAt(text, high)) {
1287
+ bracketed = true;
1288
+ break;
1289
+ }
1290
+ }
1291
+ if (!bracketed) return high;
1292
+ for (let tries = 0; tries < SEARCH_TRIES && high - low > TEXT_FIT_SLACK; tries += 1) {
1293
+ const mid = (low + high) / 2;
1294
+ if (wrapsAt(text, mid)) low = mid;
1295
+ else high = mid;
1296
+ }
1297
+ if (text.width !== high) wrapsAt(text, high);
1298
+ return high;
1299
+ }
1300
+ function restoreWidth(text, width) {
1301
+ try {
1302
+ text.set({ width });
1303
+ text.initDimensions?.();
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ function isSettled(text, width) {
1308
+ if (wrapsAt(text, width)) return false;
1309
+ const tight = wrapsAt(text, width - TEXT_FIT_SLACK);
1310
+ wrapsAt(text, width);
1311
+ return tight;
1312
+ }
1265
1313
  function fitTextWidth(object) {
1266
1314
  const text = asText(object);
1267
1315
  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;
1316
+ const authored = text.width;
1317
+ try {
1318
+ const before = textInk(text);
1319
+ const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1320
+ if (Math.abs(fitted - authored) < TEXT_FIT_SLACK) return false;
1321
+ const nearFit = authored > fitted && authored <= fitted * GROWTH_FACTOR + 1;
1322
+ if (nearFit && isSettled(text, authored)) return false;
1323
+ const centre = text.getCenterPoint();
1324
+ const settled = widenPastSoftWrap(text, fitted);
1325
+ const after = textInk(text);
1326
+ const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1327
+ const radians = (text.angle ?? 0) * Math.PI / 180;
1328
+ const moved = new import_fabric4.Point(
1329
+ centre.x + shift * Math.cos(radians),
1330
+ centre.y + shift * Math.sin(radians)
1331
+ );
1332
+ text.setPositionByOrigin(moved, "center", "center");
1333
+ text.setCoords();
1334
+ text.dirty = true;
1335
+ return Math.abs(settled - authored) >= TEXT_FIT_SLACK;
1336
+ } catch (error) {
1337
+ restoreWidth(text, authored);
1338
+ throw error;
1339
+ }
1285
1340
  }
1286
1341
 
1287
1342
  // src/pattern/tile-geometry.ts
@@ -2339,8 +2394,268 @@ var TextCurveManager = class {
2339
2394
  }
2340
2395
  };
2341
2396
 
2342
- // src/mask-presets/manager.ts
2397
+ // src/text-wrap.ts
2398
+ var import_fabric9 = require("fabric");
2399
+
2400
+ // src/text-wrap-split.ts
2401
+ var WHITESPACE = /[ \t\r]/;
2402
+ function preWrapWordSplit(value) {
2403
+ const tokens = [];
2404
+ let index = 0;
2405
+ let first = true;
2406
+ while (index < value.length) {
2407
+ let space = "";
2408
+ while (index < value.length && WHITESPACE.test(value[index])) {
2409
+ space += value[index];
2410
+ index += 1;
2411
+ }
2412
+ let word = "";
2413
+ while (index < value.length && !WHITESPACE.test(value[index])) {
2414
+ word += value[index];
2415
+ index += 1;
2416
+ }
2417
+ tokens.push((first ? space : space.slice(1)) + word);
2418
+ first = false;
2419
+ }
2420
+ return tokens.length > 0 ? tokens : [""];
2421
+ }
2422
+
2423
+ // src/masks/space.ts
2343
2424
  var import_fabric8 = require("fabric");
2425
+ function matrixOf(object) {
2426
+ return object.calcTransformMatrix();
2427
+ }
2428
+ function applyMatrix(object, matrix) {
2429
+ const decomposed = import_fabric8.util.qrDecompose(matrix);
2430
+ object.set({
2431
+ flipX: false,
2432
+ flipY: false,
2433
+ originX: "center",
2434
+ originY: "center",
2435
+ left: decomposed.translateX,
2436
+ top: decomposed.translateY,
2437
+ scaleX: decomposed.scaleX,
2438
+ scaleY: decomposed.scaleY,
2439
+ angle: decomposed.angle,
2440
+ skewX: decomposed.skewX,
2441
+ skewY: 0
2442
+ });
2443
+ object.setCoords();
2444
+ }
2445
+ function toCanvasSpace(object, host) {
2446
+ applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
2447
+ }
2448
+ function toHostSpace(object, host) {
2449
+ applyMatrix(
2450
+ object,
2451
+ import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object))
2452
+ );
2453
+ }
2454
+ function relativeMatrix(object, host) {
2455
+ return import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object));
2456
+ }
2457
+ function applyRelativeMatrix(object, host, rel) {
2458
+ applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), rel));
2459
+ }
2460
+ function asObject(clip) {
2461
+ return clip;
2462
+ }
2463
+ function toMatrix(values) {
2464
+ if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
2465
+ return null;
2466
+ return [values[0], values[1], values[2], values[3], values[4], values[5]];
2467
+ }
2468
+ function fitToBox(object, box, zoom = 1) {
2469
+ const width = Math.max(1, box.width) * zoom;
2470
+ const height = Math.max(1, box.height) * zoom;
2471
+ object.set({
2472
+ originX: "center",
2473
+ originY: "center",
2474
+ angle: 0,
2475
+ skewX: 0,
2476
+ skewY: 0,
2477
+ left: box.left + box.width / 2,
2478
+ top: box.top + box.height / 2,
2479
+ scaleX: width / Math.max(1, object.width ?? 1),
2480
+ scaleY: height / Math.max(1, object.height ?? 1)
2481
+ });
2482
+ object.setCoords();
2483
+ }
2484
+ function unwrapGroup(group) {
2485
+ const children = group.removeAll();
2486
+ for (const child of children) child.setCoords();
2487
+ return children;
2488
+ }
2489
+
2490
+ // src/text-wrap.ts
2491
+ function patchWordSplit(text) {
2492
+ if (Object.prototype.hasOwnProperty.call(text, "wordSplit")) return;
2493
+ Object.defineProperty(text, "wordSplit", {
2494
+ value: preWrapWordSplit,
2495
+ configurable: true,
2496
+ writable: true
2497
+ });
2498
+ }
2499
+ function unpatchWordSplit(text) {
2500
+ if (Object.prototype.hasOwnProperty.call(text, "wordSplit")) {
2501
+ delete text.wordSplit;
2502
+ }
2503
+ }
2504
+ function readTextWrap(meta) {
2505
+ return { wrap: meta?.wrap ?? "none", overflow: meta?.overflow ?? "visible" };
2506
+ }
2507
+ function boxClip(text, makeRect) {
2508
+ return makeRect({
2509
+ width: Math.max(1, text.width ?? 1),
2510
+ height: Math.max(1, text.height ?? 1),
2511
+ originX: "center",
2512
+ originY: "center",
2513
+ left: 0,
2514
+ top: 0,
2515
+ objectCaching: false
2516
+ });
2517
+ }
2518
+ function hasMask(meta) {
2519
+ return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;
2520
+ }
2521
+ function applyTextWrapToObject(object, meta, makeRect = (options) => new import_fabric9.Rect(options)) {
2522
+ const text = asText(object);
2523
+ if (!text) return;
2524
+ const state = readTextWrap(meta);
2525
+ if (!text.path) {
2526
+ text.set({ splitByGrapheme: state.wrap === "break-all" });
2527
+ if (state.wrap === "pre-wrap") patchWordSplit(text);
2528
+ else unpatchWordSplit(text);
2529
+ text.initDimensions?.();
2530
+ }
2531
+ const clip = state.overflow === "hidden" ? boxClip(text, makeRect) : void 0;
2532
+ if (meta && hasMask(meta)) {
2533
+ const host = text.clipPath;
2534
+ if (host) {
2535
+ if (clip) toHostSpace(clip, asObject(host));
2536
+ host.clipPath = clip;
2537
+ }
2538
+ } else {
2539
+ text.clipPath = clip;
2540
+ }
2541
+ }
2542
+ var TextWrapManager = class {
2543
+ constructor(canvas, layers, history, events) {
2544
+ this.canvas = canvas;
2545
+ this.layers = layers;
2546
+ this.history = history;
2547
+ this.events = events;
2548
+ this.canvas.on("text:changed", this.onTextChanged);
2549
+ this.events.on("masks:changed", this.onMasksChanged);
2550
+ }
2551
+ canvas;
2552
+ layers;
2553
+ history;
2554
+ events;
2555
+ /**
2556
+ * Typing changes the run the box was fitted to, so an auto-width layer has to
2557
+ * re-fit. No history entry: fabric records the edit when editing exits, and a
2558
+ * save per keystroke would bury every earlier step.
2559
+ */
2560
+ onTextChanged = (event) => {
2561
+ const layer = event.target ? this.layers.findByObject(event.target) : void 0;
2562
+ if (!layer) return;
2563
+ this.refresh(layer.id, false);
2564
+ };
2565
+ /**
2566
+ * Both mask owners — the preset manager and every mask-stack mutation —
2567
+ * install their clip straight onto `clipPath`, dropping whatever was there,
2568
+ * and neither knows this layer had a box clip. Re-deriving on the one event
2569
+ * they both announce puts it back where it now belongs: nested under the new
2570
+ * mask, or at the top level when the last mask leaves. Waiting for the next
2571
+ * keystroke instead would leave a layer that should clip inside its box
2572
+ * serialized unclipped — which is what the print renderer reads.
2573
+ */
2574
+ onMasksChanged = ({ target }) => {
2575
+ this.refresh(target);
2576
+ };
2577
+ dispose() {
2578
+ this.canvas.off("text:changed", this.onTextChanged);
2579
+ this.events.off("masks:changed", this.onMasksChanged);
2580
+ }
2581
+ /** Both properties for a text layer, or null when it is not text. */
2582
+ get(layerId) {
2583
+ const layer = this.layers.get(layerId);
2584
+ if (!layer || !asText(layer.fabricObject)) return null;
2585
+ return readTextWrap(layer.meta);
2586
+ }
2587
+ apply(layerId, wrap, save = true) {
2588
+ const layer = this.layers.get(layerId);
2589
+ const text = layer ? asText(layer.fabricObject) : null;
2590
+ if (!layer || !text) return false;
2591
+ if (wrap === "none") {
2592
+ const curved = !!text.path;
2593
+ if (layer.meta.wrapWidth === void 0 && !curved) {
2594
+ layer.meta.wrapWidth = text.width ?? 0;
2595
+ }
2596
+ }
2597
+ layer.meta.wrap = wrap;
2598
+ return this.derive(layerId, save);
2599
+ }
2600
+ setOverflow(layerId, overflow, save = true) {
2601
+ const layer = this.layers.get(layerId);
2602
+ if (!layer || !asText(layer.fabricObject)) return false;
2603
+ layer.meta.overflow = overflow;
2604
+ return this.derive(layerId, save);
2605
+ }
2606
+ /** Back to the defaults: auto-width, unclipped. */
2607
+ clear(layerId, save = true) {
2608
+ const layer = this.layers.get(layerId);
2609
+ if (!layer || !asText(layer.fabricObject)) return false;
2610
+ delete layer.meta.overflow;
2611
+ return this.apply(layerId, "none", save);
2612
+ }
2613
+ /** Re-derive from the stored mode — after a text, font or size change. */
2614
+ refresh(layerId, save = false) {
2615
+ const layer = this.layers.get(layerId);
2616
+ if (!layer || !asText(layer.fabricObject)) return false;
2617
+ return this.derive(layerId, save);
2618
+ }
2619
+ /**
2620
+ * Re-derive every text layer — used after a state restore.
2621
+ *
2622
+ * Recursive because a template inserts as a group of real child layers, and
2623
+ * text inside one would otherwise keep whatever box it was restored with.
2624
+ */
2625
+ refreshAll() {
2626
+ const visit = (layers) => {
2627
+ for (const layer of layers) {
2628
+ if (asText(layer.fabricObject)) this.refresh(layer.id);
2629
+ if (layer.children.length > 0) visit(layer.children);
2630
+ }
2631
+ };
2632
+ visit(this.layers.getAll());
2633
+ }
2634
+ derive(layerId, save) {
2635
+ const layer = this.layers.get(layerId);
2636
+ const text = layer ? asText(layer.fabricObject) : null;
2637
+ if (!layer || !text) return false;
2638
+ const state = readTextWrap(layer.meta);
2639
+ if (!text.path) {
2640
+ if (state.wrap === "none") {
2641
+ fitTextWidth(text);
2642
+ } else if (layer.meta.wrapWidth !== void 0) {
2643
+ text.set({ width: layer.meta.wrapWidth });
2644
+ delete layer.meta.wrapWidth;
2645
+ }
2646
+ }
2647
+ applyTextWrapToObject(text, layer.meta);
2648
+ text.dirty = true;
2649
+ text.setCoords();
2650
+ this.canvas.requestRenderAll();
2651
+ this.events.emit("layer:modified", { layerId });
2652
+ if (save) this.history.save();
2653
+ return true;
2654
+ }
2655
+ };
2656
+
2657
+ // src/mask-presets/manager.ts
2658
+ var import_fabric10 = require("fabric");
2344
2659
 
2345
2660
  // src/mask-presets/shapes.ts
2346
2661
  var SHAPE_MASK_IDS = [
@@ -2573,6 +2888,7 @@ var MaskPresetManager = class {
2573
2888
  object.dirty = true;
2574
2889
  object.setCoords();
2575
2890
  this.canvas.requestRenderAll();
2891
+ this.events.emit("masks:changed", { target: layerId });
2576
2892
  this.events.emit("layer:modified", { layerId });
2577
2893
  if (save) this.history.save();
2578
2894
  return true;
@@ -2607,13 +2923,13 @@ var MaskPresetManager = class {
2607
2923
  objectCaching: false
2608
2924
  };
2609
2925
  if (isShapeMaskId(id)) {
2610
- return new import_fabric8.Path(shapeMaskPathData(id), {
2926
+ return new import_fabric10.Path(shapeMaskPathData(id), {
2611
2927
  ...shared,
2612
2928
  scaleX: width / SHAPE_MASK_BOX,
2613
2929
  scaleY: height / SHAPE_MASK_BOX
2614
2930
  });
2615
2931
  }
2616
- return new import_fabric8.FabricImage(renderTextureMask(id), {
2932
+ return new import_fabric10.FabricImage(renderTextureMask(id), {
2617
2933
  ...shared,
2618
2934
  scaleX: width / TEXTURE_MASK_SIZE,
2619
2935
  scaleY: height / TEXTURE_MASK_SIZE
@@ -2622,7 +2938,7 @@ var MaskPresetManager = class {
2622
2938
  };
2623
2939
 
2624
2940
  // src/shadow.ts
2625
- var import_fabric9 = require("fabric");
2941
+ var import_fabric11 = require("fabric");
2626
2942
 
2627
2943
  // src/utils/color.ts
2628
2944
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -2660,7 +2976,7 @@ function applyLayerShadow(object, config) {
2660
2976
  }
2661
2977
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2662
2978
  object.set({
2663
- shadow: new import_fabric9.Shadow({
2979
+ shadow: new import_fabric11.Shadow({
2664
2980
  color,
2665
2981
  blur: Math.max(0, next.blur),
2666
2982
  offsetX: next.offsetX,
@@ -2774,7 +3090,7 @@ var UnitConverter = class {
2774
3090
  };
2775
3091
 
2776
3092
  // src/serialization.ts
2777
- var import_fabric10 = require("fabric");
3093
+ var import_fabric12 = require("fabric");
2778
3094
  var VERSION = "2.0.0";
2779
3095
  function serializeEditor(editor) {
2780
3096
  return {
@@ -2810,7 +3126,7 @@ async function deserializeEditor(editor, state) {
2810
3126
  }
2811
3127
  const staged = await Promise.all(
2812
3128
  state.layers.map(async (serialized) => {
2813
- const fabricObject = (await import_fabric10.util.enlivenObjects([serialized.fabricObject]))[0];
3129
+ const fabricObject = (await import_fabric12.util.enlivenObjects([serialized.fabricObject]))[0];
2814
3130
  if (!fabricObject) {
2815
3131
  const source = serialized.fabricObject.src;
2816
3132
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2821,7 +3137,7 @@ async function deserializeEditor(editor, state) {
2821
3137
  return { serialized, fabricObject };
2822
3138
  })
2823
3139
  );
2824
- const stagedBackground = state.backgroundImage ? (await import_fabric10.util.enlivenObjects([state.backgroundImage]))[0] : null;
3140
+ const stagedBackground = state.backgroundImage ? (await import_fabric12.util.enlivenObjects([state.backgroundImage]))[0] : null;
2825
3141
  if (state.backgroundImage && !stagedBackground) {
2826
3142
  const source = state.backgroundImage.src;
2827
3143
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2847,6 +3163,7 @@ async function deserializeEditor(editor, state) {
2847
3163
  for (const item of staged) {
2848
3164
  restoreLayer(editor, item.serialized, item.fabricObject);
2849
3165
  }
3166
+ editor.wrap.refreshAll();
2850
3167
  editor.canvas.requestRenderAll();
2851
3168
  }
2852
3169
  function restoreLayer(editor, serialized, fabricObject) {
@@ -2873,7 +3190,7 @@ function restoreLayer(editor, serialized, fabricObject) {
2873
3190
  }
2874
3191
 
2875
3192
  // src/export.ts
2876
- var import_fabric11 = require("fabric");
3193
+ var import_fabric13 = require("fabric");
2877
3194
 
2878
3195
  // src/displacement.ts
2879
3196
  var CHANNEL_INDEX = {
@@ -2967,7 +3284,7 @@ async function exportPNG(canvas, options = {}) {
2967
3284
  }
2968
3285
  async function exportIsolatedPNG(source, objects, options = {}) {
2969
3286
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2970
- const canvas = new import_fabric11.StaticCanvas(element, {
3287
+ const canvas = new import_fabric13.StaticCanvas(element, {
2971
3288
  width: options.width ?? source.getWidth(),
2972
3289
  height: options.height ?? source.getHeight(),
2973
3290
  backgroundColor: options.backgroundColor || void 0
@@ -2997,7 +3314,7 @@ async function exportPrintArea(source, area, options = {}) {
2997
3314
  throw new Error("Print area does not overlap the canvas");
2998
3315
  }
2999
3316
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
3000
- const canvas = new import_fabric11.StaticCanvas(element, { width, height });
3317
+ const canvas = new import_fabric13.StaticCanvas(element, { width, height });
3001
3318
  try {
3002
3319
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
3003
3320
  if (clones.length) canvas.add(...clones);
@@ -3437,7 +3754,7 @@ var ProjectManager = class {
3437
3754
  };
3438
3755
 
3439
3756
  // src/mask.ts
3440
- var import_fabric12 = require("fabric");
3757
+ var import_fabric14 = require("fabric");
3441
3758
  var MaskRefinementError = class extends Error {
3442
3759
  constructor(code, message, cause) {
3443
3760
  super(message);
@@ -3469,7 +3786,7 @@ var MaskController = class {
3469
3786
  throw new Error("Mask dimensions must be positive integers");
3470
3787
  }
3471
3788
  const backing = this.makeCanvas(width, height);
3472
- const image = new import_fabric12.FabricImage(backing, {
3789
+ const image = new import_fabric14.FabricImage(backing, {
3473
3790
  left: 0,
3474
3791
  top: 0,
3475
3792
  originX: "left",
@@ -3695,10 +4012,10 @@ var MaskController = class {
3695
4012
  };
3696
4013
 
3697
4014
  // src/masks/manager.ts
3698
- var import_fabric19 = require("fabric");
4015
+ var import_fabric20 = require("fabric");
3699
4016
 
3700
4017
  // src/masks/compose.ts
3701
- var import_fabric13 = require("fabric");
4018
+ var import_fabric15 = require("fabric");
3702
4019
  var MODE_OPERATION = {
3703
4020
  add: "source-over",
3704
4021
  subtract: "destination-out",
@@ -3708,7 +4025,7 @@ function neutralize(child) {
3708
4025
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3709
4026
  }
3710
4027
  function baseRect(box) {
3711
- return new import_fabric13.Rect({
4028
+ return new import_fabric15.Rect({
3712
4029
  left: box.left,
3713
4030
  top: box.top,
3714
4031
  width: Math.max(1, box.width),
@@ -3735,7 +4052,7 @@ function composeMaskGroup(children, entries, options) {
3735
4052
  });
3736
4053
  const first = entries.find((entry) => entry.visible);
3737
4054
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3738
- return new import_fabric13.Group(withBase, {
4055
+ return new import_fabric15.Group(withBase, {
3739
4056
  absolutePositioned: options.absolute,
3740
4057
  // Cached, so the children's compositing operations resolve against each
3741
4058
  // other instead of against the page underneath the mask.
@@ -3749,76 +4066,7 @@ function needsAbsoluteSpace(entries) {
3749
4066
  }
3750
4067
 
3751
4068
  // src/masks/edit.ts
3752
- var import_fabric15 = require("fabric");
3753
-
3754
- // src/masks/space.ts
3755
- var import_fabric14 = require("fabric");
3756
- function matrixOf(object) {
3757
- return object.calcTransformMatrix();
3758
- }
3759
- function applyMatrix(object, matrix) {
3760
- const decomposed = import_fabric14.util.qrDecompose(matrix);
3761
- object.set({
3762
- flipX: false,
3763
- flipY: false,
3764
- originX: "center",
3765
- originY: "center",
3766
- left: decomposed.translateX,
3767
- top: decomposed.translateY,
3768
- scaleX: decomposed.scaleX,
3769
- scaleY: decomposed.scaleY,
3770
- angle: decomposed.angle,
3771
- skewX: decomposed.skewX,
3772
- skewY: 0
3773
- });
3774
- object.setCoords();
3775
- }
3776
- function toCanvasSpace(object, host) {
3777
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3778
- }
3779
- function toHostSpace(object, host) {
3780
- applyMatrix(
3781
- object,
3782
- import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object))
3783
- );
3784
- }
3785
- function relativeMatrix(object, host) {
3786
- return import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object));
3787
- }
3788
- function applyRelativeMatrix(object, host, rel) {
3789
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), rel));
3790
- }
3791
- function asObject(clip) {
3792
- return clip;
3793
- }
3794
- function toMatrix(values) {
3795
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3796
- return null;
3797
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3798
- }
3799
- function fitToBox(object, box, zoom = 1) {
3800
- const width = Math.max(1, box.width) * zoom;
3801
- const height = Math.max(1, box.height) * zoom;
3802
- object.set({
3803
- originX: "center",
3804
- originY: "center",
3805
- angle: 0,
3806
- skewX: 0,
3807
- skewY: 0,
3808
- left: box.left + box.width / 2,
3809
- top: box.top + box.height / 2,
3810
- scaleX: width / Math.max(1, object.width ?? 1),
3811
- scaleY: height / Math.max(1, object.height ?? 1)
3812
- });
3813
- object.setCoords();
3814
- }
3815
- function unwrapGroup(group) {
3816
- const children = group.removeAll();
3817
- for (const child of children) child.setCoords();
3818
- return children;
3819
- }
3820
-
3821
- // src/masks/edit.ts
4069
+ var import_fabric16 = require("fabric");
3822
4070
  var MaskEditController = class {
3823
4071
  constructor(canvas, onCommit) {
3824
4072
  this.canvas = canvas;
@@ -3899,8 +4147,8 @@ var MaskEditController = class {
3899
4147
  if (!this.handle || !this.child || !this.group) return;
3900
4148
  applyMatrix(
3901
4149
  this.child,
3902
- import_fabric15.util.multiplyTransformMatrices(
3903
- import_fabric15.util.invertTransform(matrixOf(this.group)),
4150
+ import_fabric16.util.multiplyTransformMatrices(
4151
+ import_fabric16.util.invertTransform(matrixOf(this.group)),
3904
4152
  matrixOf(this.handle)
3905
4153
  )
3906
4154
  );
@@ -3911,15 +4159,15 @@ var MaskEditController = class {
3911
4159
  };
3912
4160
 
3913
4161
  // src/masks/store.ts
3914
- var import_fabric18 = require("fabric");
4162
+ var import_fabric19 = require("fabric");
3915
4163
 
3916
4164
  // src/masks/host.ts
3917
- var import_fabric16 = require("fabric");
4165
+ var import_fabric17 = require("fabric");
3918
4166
  function findCanvasHost(layers) {
3919
4167
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3920
4168
  }
3921
4169
  function createCanvasHost(canvas, layers) {
3922
- const rect = new import_fabric16.Rect({
4170
+ const rect = new import_fabric17.Rect({
3923
4171
  left: 0,
3924
4172
  top: 0,
3925
4173
  width: canvas.getWidth(),
@@ -3968,9 +4216,9 @@ function hostBoxOf(canvas, host, absolute) {
3968
4216
  }
3969
4217
 
3970
4218
  // src/masks/install.ts
3971
- var import_fabric17 = require("fabric");
4219
+ var import_fabric18 = require("fabric");
3972
4220
  function convertSpace(host, sources, absolute) {
3973
- const wasAbsolute = host.clipPath instanceof import_fabric17.Group ? host.clipPath.absolutePositioned : absolute;
4221
+ const wasAbsolute = host.clipPath instanceof import_fabric18.Group ? host.clipPath.absolutePositioned : absolute;
3974
4222
  if (absolute === wasAbsolute) return;
3975
4223
  for (const source of sources) {
3976
4224
  if (absolute) toCanvasSpace(source, host);
@@ -4050,6 +4298,24 @@ var MaskStackStore = class {
4050
4298
  this.pinning = false;
4051
4299
  }
4052
4300
  }
4301
+ /**
4302
+ * Is the host's own `clipPath` the wrap manager's box clip rather than a mask?
4303
+ *
4304
+ * Adoption rescues a clip whose provenance is UNKNOWN — a mask preset, or a
4305
+ * hand-set `clipPath` from before stacks existed — so the first add() cannot
4306
+ * silently throw it away. A text layer's box clip is the opposite case: its
4307
+ * provenance is known exactly (the wrap manager derives it from
4308
+ * `meta.overflow` on every pass and re-installs it under whatever owns the
4309
+ * property next), and it is not a mask. Adopted, it becomes a stack row the
4310
+ * user never made whose 'add' union covers the whole box — swallowing the mask
4311
+ * they actually added.
4312
+ *
4313
+ * Only meaningful with an empty stack: once there are entries the top-level
4314
+ * clip is the composed group and the box clip is nested underneath it.
4315
+ */
4316
+ holdsBoxClip(meta) {
4317
+ return meta?.overflow === "hidden" && !meta.maskPreset;
4318
+ }
4053
4319
  /**
4054
4320
  * Take the current geometry back out of the composed clip, entry-aligned.
4055
4321
  *
@@ -4062,7 +4328,13 @@ var MaskStackStore = class {
4062
4328
  const clip = host.clipPath;
4063
4329
  if (!clip) return [];
4064
4330
  const entries = this.list(target);
4065
- if (entries.length === 0 || !(clip instanceof import_fabric18.Group)) return [asObject(clip)];
4331
+ const meta = this.hostLayer(target)?.meta;
4332
+ if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
4333
+ if (entries.length === 0 || !(clip instanceof import_fabric19.Group)) {
4334
+ const source = asObject(clip);
4335
+ if (meta?.overflow === "hidden") source.clipPath = void 0;
4336
+ return [source];
4337
+ }
4066
4338
  const children = unwrapGroup(clip);
4067
4339
  const extra = children.length - entries.length;
4068
4340
  return extra > 0 ? children.slice(extra) : children;
@@ -4162,7 +4434,7 @@ var LayerMaskManager = class extends MaskStackStore {
4162
4434
  if (index === -1) return false;
4163
4435
  this.endEdit(false);
4164
4436
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4165
- const group = host.clipPath instanceof import_fabric19.Group ? host.clipPath : null;
4437
+ const group = host.clipPath instanceof import_fabric20.Group ? host.clipPath : null;
4166
4438
  if (!group) return false;
4167
4439
  const children = group.getObjects();
4168
4440
  const child = children[children.length - entries.length + index];
@@ -4393,6 +4665,7 @@ var LayerMaskManager = class extends MaskStackStore {
4393
4665
  const clip = layer?.fabricObject.clipPath;
4394
4666
  if (!layer || !clip || layer.meta.pattern) return null;
4395
4667
  if ((layer.meta.maskStack ?? []).length > 0) return null;
4668
+ if (this.holdsBoxClip(layer.meta)) return null;
4396
4669
  const entry = {
4397
4670
  ...DEFAULT_ENTRY,
4398
4671
  id: generateId(),
@@ -4445,6 +4718,7 @@ var CanvasEditor = class {
4445
4718
  crop;
4446
4719
  patterns;
4447
4720
  curves;
4721
+ wrap;
4448
4722
  maskPresets;
4449
4723
  /** Stacked boolean masks, per layer and for the design as a whole. */
4450
4724
  layerMasks;
@@ -4472,7 +4746,7 @@ var CanvasEditor = class {
4472
4746
  const widthPx = this.units.toPixels(config.width);
4473
4747
  const heightPx = this.units.toPixels(config.height);
4474
4748
  this.designBackground = config.backgroundColor ?? "#ffffff";
4475
- this.canvas = new import_fabric20.Canvas(canvasElement, {
4749
+ this.canvas = new import_fabric21.Canvas(canvasElement, {
4476
4750
  width: widthPx,
4477
4751
  height: heightPx,
4478
4752
  backgroundColor: this.designBackground,
@@ -4495,6 +4769,7 @@ var CanvasEditor = class {
4495
4769
  this.crop = new CropController(this.canvas, this.history, this.events);
4496
4770
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4497
4771
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
4772
+ this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
4498
4773
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4499
4774
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4500
4775
  this.setupCanvasEvents();
@@ -4506,7 +4781,7 @@ var CanvasEditor = class {
4506
4781
  // ─── Layer Operations ────────────────────────────────
4507
4782
  async addImage(url, options) {
4508
4783
  try {
4509
- const img = await import_fabric20.FabricImage.fromURL(
4784
+ const img = await import_fabric21.FabricImage.fromURL(
4510
4785
  url,
4511
4786
  {},
4512
4787
  { originX: "left", originY: "top", ...options }
@@ -4532,7 +4807,7 @@ var CanvasEditor = class {
4532
4807
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4533
4808
  const previous = layer.fabricObject;
4534
4809
  try {
4535
- const replacement = await import_fabric20.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4810
+ const replacement = await import_fabric21.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4536
4811
  replacement.set({
4537
4812
  left: previous.left,
4538
4813
  top: previous.top,
@@ -4561,7 +4836,7 @@ var CanvasEditor = class {
4561
4836
  }
4562
4837
  }
4563
4838
  addText(text, options) {
4564
- const textbox = new import_fabric20.Textbox(text, {
4839
+ const textbox = new import_fabric21.Textbox(text, {
4565
4840
  fontSize: 32,
4566
4841
  fontFamily: "Arial",
4567
4842
  fill: "#000000",
@@ -4692,10 +4967,10 @@ var CanvasEditor = class {
4692
4967
  const next = { ...previous, ...adjustments };
4693
4968
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4694
4969
  image.filters = [
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) })
4970
+ new import_fabric21.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4971
+ new import_fabric21.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4972
+ new import_fabric21.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4973
+ new import_fabric21.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4699
4974
  ];
4700
4975
  layer.meta.imageAdjustments = next;
4701
4976
  image.applyFilters();
@@ -4736,7 +5011,7 @@ var CanvasEditor = class {
4736
5011
  this.layers.detach(layer.id);
4737
5012
  }
4738
5013
  }
4739
- const group = new import_fabric20.Group(objects);
5014
+ const group = new import_fabric21.Group(objects);
4740
5015
  const grouped = this.layers.add("group", group, name);
4741
5016
  this.layers.adoptChildren(grouped, children);
4742
5017
  this.layers.reorder(grouped.id, insertIndex);
@@ -4798,7 +5073,7 @@ var CanvasEditor = class {
4798
5073
  const layer = this.layers.get(id);
4799
5074
  if (!layer) throw new Error(`Layer not found: ${id}`);
4800
5075
  try {
4801
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric20.FabricImage) {
5076
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric21.FabricImage) {
4802
5077
  const image = await layer.fabricObject.clone();
4803
5078
  image.set({
4804
5079
  left: 0,
@@ -5074,7 +5349,7 @@ var CanvasEditor = class {
5074
5349
  return;
5075
5350
  }
5076
5351
  try {
5077
- const image = await import_fabric20.FabricImage.fromURL(
5352
+ const image = await import_fabric21.FabricImage.fromURL(
5078
5353
  url,
5079
5354
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5080
5355
  { originX: "left", originY: "top" }
@@ -5251,15 +5526,34 @@ var CanvasEditor = class {
5251
5526
  }
5252
5527
  // ─── Text curve ─────────────────────────────────────
5253
5528
  applyTextCurve(layerId, config) {
5254
- return this.curves.apply(layerId, config);
5529
+ const applied = this.curves.apply(layerId, config);
5530
+ if (applied) this.wrap.refresh(layerId);
5531
+ return applied;
5255
5532
  }
5256
5533
  clearTextCurve(layerId) {
5257
- return this.curves.clear(layerId);
5534
+ const cleared = this.curves.clear(layerId);
5535
+ if (cleared) this.wrap.refresh(layerId);
5536
+ return cleared;
5258
5537
  }
5259
5538
  getTextCurve(layerId) {
5260
5539
  return this.curves.get(layerId);
5261
5540
  }
5541
+ // ─── Text wrap ──────────────────────────────────────
5542
+ applyTextWrap(layerId, wrap) {
5543
+ return this.wrap.apply(layerId, wrap);
5544
+ }
5545
+ setTextOverflow(layerId, overflow) {
5546
+ return this.wrap.setOverflow(layerId, overflow);
5547
+ }
5548
+ getTextWrap(layerId) {
5549
+ return this.wrap.get(layerId);
5550
+ }
5262
5551
  // ─── Mask presets ───────────────────────────────────
5552
+ // A preset assigns `clipPath` wholesale, so a text layer clipping to its box
5553
+ // loses that clip the moment a mask lands on it. Putting it back is NOT wired
5554
+ // here: the preset manager announces `masks:changed` and the wrap manager
5555
+ // re-derives from that, which covers this facade, the manager called directly,
5556
+ // and `refreshAll()` on a restore — all with one mechanism.
5263
5557
  applyMaskPreset(layerId, id) {
5264
5558
  return this.maskPresets.apply(layerId, id);
5265
5559
  }
@@ -5348,6 +5642,7 @@ var CanvasEditor = class {
5348
5642
  this.history.dispose();
5349
5643
  this.patterns.dispose();
5350
5644
  this.curves.dispose();
5645
+ this.wrap.dispose();
5351
5646
  this.events.removeAllListeners();
5352
5647
  this.canvas.dispose();
5353
5648
  }
@@ -5497,12 +5792,14 @@ var AnnotationOverlay = class {
5497
5792
  TEXTURE_MASK_IDS,
5498
5793
  TEXTURE_MASK_SIZE,
5499
5794
  TextCurveManager,
5795
+ TextWrapManager,
5500
5796
  TiledPatternObject,
5501
5797
  UnitConverter,
5502
5798
  applyAspectLock,
5503
5799
  applyLayerShadow,
5504
5800
  applyObjectSelectionStyle,
5505
5801
  applySelectionStyle,
5802
+ applyTextWrapToObject,
5506
5803
  buildCurveLinePaths,
5507
5804
  buildCurvePathData,
5508
5805
  clamp,
@@ -5530,7 +5827,9 @@ var AnnotationOverlay = class {
5530
5827
  isTextureMaskId,
5531
5828
  needsAbsoluteSpace,
5532
5829
  normalizeTextCurve,
5830
+ preWrapWordSplit,
5533
5831
  readLayerShadow,
5832
+ readTextWrap,
5534
5833
  renderTextureMask,
5535
5834
  resetTransform,
5536
5835
  restoreLocks,