@overtone-art/canvas-editor-core 0.6.3 → 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
@@ -1961,7 +2016,11 @@ function normalizeTextCurve(config) {
1961
2016
  shape: config.shape ?? inferred,
1962
2017
  arc,
1963
2018
  wave,
1964
- waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
2019
+ waveLength: clamp(
2020
+ config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
2021
+ MIN_WAVE_LENGTH,
2022
+ MAX_WAVE_LENGTH
2023
+ ),
1965
2024
  offset: clamp(config.offset ?? 0, -100, 100),
1966
2025
  centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
1967
2026
  };
@@ -2335,8 +2394,268 @@ var TextCurveManager = class {
2335
2394
  }
2336
2395
  };
2337
2396
 
2338
- // 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
2339
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");
2340
2659
 
2341
2660
  // src/mask-presets/shapes.ts
2342
2661
  var SHAPE_MASK_IDS = [
@@ -2569,6 +2888,7 @@ var MaskPresetManager = class {
2569
2888
  object.dirty = true;
2570
2889
  object.setCoords();
2571
2890
  this.canvas.requestRenderAll();
2891
+ this.events.emit("masks:changed", { target: layerId });
2572
2892
  this.events.emit("layer:modified", { layerId });
2573
2893
  if (save) this.history.save();
2574
2894
  return true;
@@ -2603,13 +2923,13 @@ var MaskPresetManager = class {
2603
2923
  objectCaching: false
2604
2924
  };
2605
2925
  if (isShapeMaskId(id)) {
2606
- return new import_fabric8.Path(shapeMaskPathData(id), {
2926
+ return new import_fabric10.Path(shapeMaskPathData(id), {
2607
2927
  ...shared,
2608
2928
  scaleX: width / SHAPE_MASK_BOX,
2609
2929
  scaleY: height / SHAPE_MASK_BOX
2610
2930
  });
2611
2931
  }
2612
- return new import_fabric8.FabricImage(renderTextureMask(id), {
2932
+ return new import_fabric10.FabricImage(renderTextureMask(id), {
2613
2933
  ...shared,
2614
2934
  scaleX: width / TEXTURE_MASK_SIZE,
2615
2935
  scaleY: height / TEXTURE_MASK_SIZE
@@ -2618,7 +2938,7 @@ var MaskPresetManager = class {
2618
2938
  };
2619
2939
 
2620
2940
  // src/shadow.ts
2621
- var import_fabric9 = require("fabric");
2941
+ var import_fabric11 = require("fabric");
2622
2942
 
2623
2943
  // src/utils/color.ts
2624
2944
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -2656,7 +2976,7 @@ function applyLayerShadow(object, config) {
2656
2976
  }
2657
2977
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2658
2978
  object.set({
2659
- shadow: new import_fabric9.Shadow({
2979
+ shadow: new import_fabric11.Shadow({
2660
2980
  color,
2661
2981
  blur: Math.max(0, next.blur),
2662
2982
  offsetX: next.offsetX,
@@ -2770,7 +3090,7 @@ var UnitConverter = class {
2770
3090
  };
2771
3091
 
2772
3092
  // src/serialization.ts
2773
- var import_fabric10 = require("fabric");
3093
+ var import_fabric12 = require("fabric");
2774
3094
  var VERSION = "2.0.0";
2775
3095
  function serializeEditor(editor) {
2776
3096
  return {
@@ -2806,7 +3126,7 @@ async function deserializeEditor(editor, state) {
2806
3126
  }
2807
3127
  const staged = await Promise.all(
2808
3128
  state.layers.map(async (serialized) => {
2809
- const fabricObject = (await import_fabric10.util.enlivenObjects([serialized.fabricObject]))[0];
3129
+ const fabricObject = (await import_fabric12.util.enlivenObjects([serialized.fabricObject]))[0];
2810
3130
  if (!fabricObject) {
2811
3131
  const source = serialized.fabricObject.src;
2812
3132
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2817,7 +3137,7 @@ async function deserializeEditor(editor, state) {
2817
3137
  return { serialized, fabricObject };
2818
3138
  })
2819
3139
  );
2820
- 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;
2821
3141
  if (state.backgroundImage && !stagedBackground) {
2822
3142
  const source = state.backgroundImage.src;
2823
3143
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2843,6 +3163,7 @@ async function deserializeEditor(editor, state) {
2843
3163
  for (const item of staged) {
2844
3164
  restoreLayer(editor, item.serialized, item.fabricObject);
2845
3165
  }
3166
+ editor.wrap.refreshAll();
2846
3167
  editor.canvas.requestRenderAll();
2847
3168
  }
2848
3169
  function restoreLayer(editor, serialized, fabricObject) {
@@ -2869,7 +3190,7 @@ function restoreLayer(editor, serialized, fabricObject) {
2869
3190
  }
2870
3191
 
2871
3192
  // src/export.ts
2872
- var import_fabric11 = require("fabric");
3193
+ var import_fabric13 = require("fabric");
2873
3194
 
2874
3195
  // src/displacement.ts
2875
3196
  var CHANNEL_INDEX = {
@@ -2963,7 +3284,7 @@ async function exportPNG(canvas, options = {}) {
2963
3284
  }
2964
3285
  async function exportIsolatedPNG(source, objects, options = {}) {
2965
3286
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2966
- const canvas = new import_fabric11.StaticCanvas(element, {
3287
+ const canvas = new import_fabric13.StaticCanvas(element, {
2967
3288
  width: options.width ?? source.getWidth(),
2968
3289
  height: options.height ?? source.getHeight(),
2969
3290
  backgroundColor: options.backgroundColor || void 0
@@ -2993,7 +3314,7 @@ async function exportPrintArea(source, area, options = {}) {
2993
3314
  throw new Error("Print area does not overlap the canvas");
2994
3315
  }
2995
3316
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2996
- const canvas = new import_fabric11.StaticCanvas(element, { width, height });
3317
+ const canvas = new import_fabric13.StaticCanvas(element, { width, height });
2997
3318
  try {
2998
3319
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
2999
3320
  if (clones.length) canvas.add(...clones);
@@ -3433,7 +3754,7 @@ var ProjectManager = class {
3433
3754
  };
3434
3755
 
3435
3756
  // src/mask.ts
3436
- var import_fabric12 = require("fabric");
3757
+ var import_fabric14 = require("fabric");
3437
3758
  var MaskRefinementError = class extends Error {
3438
3759
  constructor(code, message, cause) {
3439
3760
  super(message);
@@ -3465,7 +3786,7 @@ var MaskController = class {
3465
3786
  throw new Error("Mask dimensions must be positive integers");
3466
3787
  }
3467
3788
  const backing = this.makeCanvas(width, height);
3468
- const image = new import_fabric12.FabricImage(backing, {
3789
+ const image = new import_fabric14.FabricImage(backing, {
3469
3790
  left: 0,
3470
3791
  top: 0,
3471
3792
  originX: "left",
@@ -3691,10 +4012,10 @@ var MaskController = class {
3691
4012
  };
3692
4013
 
3693
4014
  // src/masks/manager.ts
3694
- var import_fabric19 = require("fabric");
4015
+ var import_fabric20 = require("fabric");
3695
4016
 
3696
4017
  // src/masks/compose.ts
3697
- var import_fabric13 = require("fabric");
4018
+ var import_fabric15 = require("fabric");
3698
4019
  var MODE_OPERATION = {
3699
4020
  add: "source-over",
3700
4021
  subtract: "destination-out",
@@ -3704,7 +4025,7 @@ function neutralize(child) {
3704
4025
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3705
4026
  }
3706
4027
  function baseRect(box) {
3707
- return new import_fabric13.Rect({
4028
+ return new import_fabric15.Rect({
3708
4029
  left: box.left,
3709
4030
  top: box.top,
3710
4031
  width: Math.max(1, box.width),
@@ -3731,7 +4052,7 @@ function composeMaskGroup(children, entries, options) {
3731
4052
  });
3732
4053
  const first = entries.find((entry) => entry.visible);
3733
4054
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3734
- return new import_fabric13.Group(withBase, {
4055
+ return new import_fabric15.Group(withBase, {
3735
4056
  absolutePositioned: options.absolute,
3736
4057
  // Cached, so the children's compositing operations resolve against each
3737
4058
  // other instead of against the page underneath the mask.
@@ -3745,76 +4066,7 @@ function needsAbsoluteSpace(entries) {
3745
4066
  }
3746
4067
 
3747
4068
  // src/masks/edit.ts
3748
- var import_fabric15 = require("fabric");
3749
-
3750
- // src/masks/space.ts
3751
- var import_fabric14 = require("fabric");
3752
- function matrixOf(object) {
3753
- return object.calcTransformMatrix();
3754
- }
3755
- function applyMatrix(object, matrix) {
3756
- const decomposed = import_fabric14.util.qrDecompose(matrix);
3757
- object.set({
3758
- flipX: false,
3759
- flipY: false,
3760
- originX: "center",
3761
- originY: "center",
3762
- left: decomposed.translateX,
3763
- top: decomposed.translateY,
3764
- scaleX: decomposed.scaleX,
3765
- scaleY: decomposed.scaleY,
3766
- angle: decomposed.angle,
3767
- skewX: decomposed.skewX,
3768
- skewY: 0
3769
- });
3770
- object.setCoords();
3771
- }
3772
- function toCanvasSpace(object, host) {
3773
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3774
- }
3775
- function toHostSpace(object, host) {
3776
- applyMatrix(
3777
- object,
3778
- import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object))
3779
- );
3780
- }
3781
- function relativeMatrix(object, host) {
3782
- return import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object));
3783
- }
3784
- function applyRelativeMatrix(object, host, rel) {
3785
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), rel));
3786
- }
3787
- function asObject(clip) {
3788
- return clip;
3789
- }
3790
- function toMatrix(values) {
3791
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3792
- return null;
3793
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3794
- }
3795
- function fitToBox(object, box, zoom = 1) {
3796
- const width = Math.max(1, box.width) * zoom;
3797
- const height = Math.max(1, box.height) * zoom;
3798
- object.set({
3799
- originX: "center",
3800
- originY: "center",
3801
- angle: 0,
3802
- skewX: 0,
3803
- skewY: 0,
3804
- left: box.left + box.width / 2,
3805
- top: box.top + box.height / 2,
3806
- scaleX: width / Math.max(1, object.width ?? 1),
3807
- scaleY: height / Math.max(1, object.height ?? 1)
3808
- });
3809
- object.setCoords();
3810
- }
3811
- function unwrapGroup(group) {
3812
- const children = group.removeAll();
3813
- for (const child of children) child.setCoords();
3814
- return children;
3815
- }
3816
-
3817
- // src/masks/edit.ts
4069
+ var import_fabric16 = require("fabric");
3818
4070
  var MaskEditController = class {
3819
4071
  constructor(canvas, onCommit) {
3820
4072
  this.canvas = canvas;
@@ -3895,8 +4147,8 @@ var MaskEditController = class {
3895
4147
  if (!this.handle || !this.child || !this.group) return;
3896
4148
  applyMatrix(
3897
4149
  this.child,
3898
- import_fabric15.util.multiplyTransformMatrices(
3899
- import_fabric15.util.invertTransform(matrixOf(this.group)),
4150
+ import_fabric16.util.multiplyTransformMatrices(
4151
+ import_fabric16.util.invertTransform(matrixOf(this.group)),
3900
4152
  matrixOf(this.handle)
3901
4153
  )
3902
4154
  );
@@ -3907,15 +4159,15 @@ var MaskEditController = class {
3907
4159
  };
3908
4160
 
3909
4161
  // src/masks/store.ts
3910
- var import_fabric18 = require("fabric");
4162
+ var import_fabric19 = require("fabric");
3911
4163
 
3912
4164
  // src/masks/host.ts
3913
- var import_fabric16 = require("fabric");
4165
+ var import_fabric17 = require("fabric");
3914
4166
  function findCanvasHost(layers) {
3915
4167
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3916
4168
  }
3917
4169
  function createCanvasHost(canvas, layers) {
3918
- const rect = new import_fabric16.Rect({
4170
+ const rect = new import_fabric17.Rect({
3919
4171
  left: 0,
3920
4172
  top: 0,
3921
4173
  width: canvas.getWidth(),
@@ -3964,9 +4216,9 @@ function hostBoxOf(canvas, host, absolute) {
3964
4216
  }
3965
4217
 
3966
4218
  // src/masks/install.ts
3967
- var import_fabric17 = require("fabric");
4219
+ var import_fabric18 = require("fabric");
3968
4220
  function convertSpace(host, sources, absolute) {
3969
- 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;
3970
4222
  if (absolute === wasAbsolute) return;
3971
4223
  for (const source of sources) {
3972
4224
  if (absolute) toCanvasSpace(source, host);
@@ -4046,6 +4298,24 @@ var MaskStackStore = class {
4046
4298
  this.pinning = false;
4047
4299
  }
4048
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
+ }
4049
4319
  /**
4050
4320
  * Take the current geometry back out of the composed clip, entry-aligned.
4051
4321
  *
@@ -4058,7 +4328,13 @@ var MaskStackStore = class {
4058
4328
  const clip = host.clipPath;
4059
4329
  if (!clip) return [];
4060
4330
  const entries = this.list(target);
4061
- 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
+ }
4062
4338
  const children = unwrapGroup(clip);
4063
4339
  const extra = children.length - entries.length;
4064
4340
  return extra > 0 ? children.slice(extra) : children;
@@ -4158,7 +4434,7 @@ var LayerMaskManager = class extends MaskStackStore {
4158
4434
  if (index === -1) return false;
4159
4435
  this.endEdit(false);
4160
4436
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4161
- const group = host.clipPath instanceof import_fabric19.Group ? host.clipPath : null;
4437
+ const group = host.clipPath instanceof import_fabric20.Group ? host.clipPath : null;
4162
4438
  if (!group) return false;
4163
4439
  const children = group.getObjects();
4164
4440
  const child = children[children.length - entries.length + index];
@@ -4339,6 +4615,9 @@ var LayerMaskManager = class extends MaskStackStore {
4339
4615
  if (resolved === layerId) return null;
4340
4616
  const object = layer.fabricObject;
4341
4617
  if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
4618
+ const stackHost = this.host(resolved);
4619
+ const absolute = resolved === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(resolved));
4620
+ if (!absolute && stackHost) toHostSpace(object, stackHost);
4342
4621
  this.history.beginTransaction();
4343
4622
  try {
4344
4623
  if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
@@ -4386,6 +4665,7 @@ var LayerMaskManager = class extends MaskStackStore {
4386
4665
  const clip = layer?.fabricObject.clipPath;
4387
4666
  if (!layer || !clip || layer.meta.pattern) return null;
4388
4667
  if ((layer.meta.maskStack ?? []).length > 0) return null;
4668
+ if (this.holdsBoxClip(layer.meta)) return null;
4389
4669
  const entry = {
4390
4670
  ...DEFAULT_ENTRY,
4391
4671
  id: generateId(),
@@ -4438,6 +4718,7 @@ var CanvasEditor = class {
4438
4718
  crop;
4439
4719
  patterns;
4440
4720
  curves;
4721
+ wrap;
4441
4722
  maskPresets;
4442
4723
  /** Stacked boolean masks, per layer and for the design as a whole. */
4443
4724
  layerMasks;
@@ -4465,7 +4746,7 @@ var CanvasEditor = class {
4465
4746
  const widthPx = this.units.toPixels(config.width);
4466
4747
  const heightPx = this.units.toPixels(config.height);
4467
4748
  this.designBackground = config.backgroundColor ?? "#ffffff";
4468
- this.canvas = new import_fabric20.Canvas(canvasElement, {
4749
+ this.canvas = new import_fabric21.Canvas(canvasElement, {
4469
4750
  width: widthPx,
4470
4751
  height: heightPx,
4471
4752
  backgroundColor: this.designBackground,
@@ -4488,6 +4769,7 @@ var CanvasEditor = class {
4488
4769
  this.crop = new CropController(this.canvas, this.history, this.events);
4489
4770
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4490
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);
4491
4773
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4492
4774
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4493
4775
  this.setupCanvasEvents();
@@ -4499,7 +4781,7 @@ var CanvasEditor = class {
4499
4781
  // ─── Layer Operations ────────────────────────────────
4500
4782
  async addImage(url, options) {
4501
4783
  try {
4502
- const img = await import_fabric20.FabricImage.fromURL(
4784
+ const img = await import_fabric21.FabricImage.fromURL(
4503
4785
  url,
4504
4786
  {},
4505
4787
  { originX: "left", originY: "top", ...options }
@@ -4525,7 +4807,7 @@ var CanvasEditor = class {
4525
4807
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4526
4808
  const previous = layer.fabricObject;
4527
4809
  try {
4528
- 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" });
4529
4811
  replacement.set({
4530
4812
  left: previous.left,
4531
4813
  top: previous.top,
@@ -4554,7 +4836,7 @@ var CanvasEditor = class {
4554
4836
  }
4555
4837
  }
4556
4838
  addText(text, options) {
4557
- const textbox = new import_fabric20.Textbox(text, {
4839
+ const textbox = new import_fabric21.Textbox(text, {
4558
4840
  fontSize: 32,
4559
4841
  fontFamily: "Arial",
4560
4842
  fill: "#000000",
@@ -4685,10 +4967,10 @@ var CanvasEditor = class {
4685
4967
  const next = { ...previous, ...adjustments };
4686
4968
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4687
4969
  image.filters = [
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) })
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) })
4692
4974
  ];
4693
4975
  layer.meta.imageAdjustments = next;
4694
4976
  image.applyFilters();
@@ -4729,7 +5011,7 @@ var CanvasEditor = class {
4729
5011
  this.layers.detach(layer.id);
4730
5012
  }
4731
5013
  }
4732
- const group = new import_fabric20.Group(objects);
5014
+ const group = new import_fabric21.Group(objects);
4733
5015
  const grouped = this.layers.add("group", group, name);
4734
5016
  this.layers.adoptChildren(grouped, children);
4735
5017
  this.layers.reorder(grouped.id, insertIndex);
@@ -4791,7 +5073,7 @@ var CanvasEditor = class {
4791
5073
  const layer = this.layers.get(id);
4792
5074
  if (!layer) throw new Error(`Layer not found: ${id}`);
4793
5075
  try {
4794
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric20.FabricImage) {
5076
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric21.FabricImage) {
4795
5077
  const image = await layer.fabricObject.clone();
4796
5078
  image.set({
4797
5079
  left: 0,
@@ -5067,7 +5349,7 @@ var CanvasEditor = class {
5067
5349
  return;
5068
5350
  }
5069
5351
  try {
5070
- const image = await import_fabric20.FabricImage.fromURL(
5352
+ const image = await import_fabric21.FabricImage.fromURL(
5071
5353
  url,
5072
5354
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5073
5355
  { originX: "left", originY: "top" }
@@ -5244,15 +5526,34 @@ var CanvasEditor = class {
5244
5526
  }
5245
5527
  // ─── Text curve ─────────────────────────────────────
5246
5528
  applyTextCurve(layerId, config) {
5247
- return this.curves.apply(layerId, config);
5529
+ const applied = this.curves.apply(layerId, config);
5530
+ if (applied) this.wrap.refresh(layerId);
5531
+ return applied;
5248
5532
  }
5249
5533
  clearTextCurve(layerId) {
5250
- return this.curves.clear(layerId);
5534
+ const cleared = this.curves.clear(layerId);
5535
+ if (cleared) this.wrap.refresh(layerId);
5536
+ return cleared;
5251
5537
  }
5252
5538
  getTextCurve(layerId) {
5253
5539
  return this.curves.get(layerId);
5254
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
+ }
5255
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.
5256
5557
  applyMaskPreset(layerId, id) {
5257
5558
  return this.maskPresets.apply(layerId, id);
5258
5559
  }
@@ -5341,6 +5642,7 @@ var CanvasEditor = class {
5341
5642
  this.history.dispose();
5342
5643
  this.patterns.dispose();
5343
5644
  this.curves.dispose();
5645
+ this.wrap.dispose();
5344
5646
  this.events.removeAllListeners();
5345
5647
  this.canvas.dispose();
5346
5648
  }
@@ -5490,12 +5792,14 @@ var AnnotationOverlay = class {
5490
5792
  TEXTURE_MASK_IDS,
5491
5793
  TEXTURE_MASK_SIZE,
5492
5794
  TextCurveManager,
5795
+ TextWrapManager,
5493
5796
  TiledPatternObject,
5494
5797
  UnitConverter,
5495
5798
  applyAspectLock,
5496
5799
  applyLayerShadow,
5497
5800
  applyObjectSelectionStyle,
5498
5801
  applySelectionStyle,
5802
+ applyTextWrapToObject,
5499
5803
  buildCurveLinePaths,
5500
5804
  buildCurvePathData,
5501
5805
  clamp,
@@ -5523,7 +5827,9 @@ var AnnotationOverlay = class {
5523
5827
  isTextureMaskId,
5524
5828
  needsAbsoluteSpace,
5525
5829
  normalizeTextCurve,
5830
+ preWrapWordSplit,
5526
5831
  readLayerShadow,
5832
+ readTextWrap,
5527
5833
  renderTextureMask,
5528
5834
  resetTransform,
5529
5835
  restoreLocks,