@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.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  import {
2
+ TEXT_FIT_SLACK,
3
+ TextWrapManager,
4
+ applyMatrix,
5
+ applyRelativeMatrix,
6
+ applyTextWrapToObject,
7
+ asObject,
8
+ asText,
2
9
  computeCoverPlacement,
3
10
  computePrintAreaClip,
4
11
  displaceRgba,
@@ -7,8 +14,19 @@ import {
7
14
  exportMockup,
8
15
  exportPNG,
9
16
  exportPrintArea,
10
- exportSVG
11
- } from "./chunk-MCBRZQ4M.mjs";
17
+ exportSVG,
18
+ fitTextWidth,
19
+ fitToBox,
20
+ matrixOf,
21
+ preWrapWordSplit,
22
+ readTextWrap,
23
+ relativeMatrix,
24
+ textInk,
25
+ toCanvasSpace,
26
+ toHostSpace,
27
+ toMatrix,
28
+ unwrapGroup
29
+ } from "./chunk-XNGPX7FG.mjs";
12
30
 
13
31
  // src/editor.ts
14
32
  import { Canvas, FabricImage as FabricImage3, Group as Group7, Textbox as Textbox2, filters } from "fabric";
@@ -1143,60 +1161,7 @@ var STROKE2 = "#22c55e";
1143
1161
  import { util } from "fabric";
1144
1162
 
1145
1163
  // src/pattern/tiled-pattern-object.ts
1146
- import { FabricObject, Point as Point2 } from "fabric";
1147
-
1148
- // src/text-fit.ts
1149
- import { Point } from "fabric";
1150
- var TEXT_FIT_SLACK = 0.5;
1151
- var MEASURE_WIDTH = 1e5;
1152
- function unwrappedWidth(text) {
1153
- const authored = text.width;
1154
- try {
1155
- text.set({ width: MEASURE_WIDTH });
1156
- text.initDimensions?.();
1157
- const measured = text.calcTextWidth?.() ?? authored;
1158
- return Number.isFinite(measured) && measured > 0 ? measured : authored;
1159
- } finally {
1160
- text.set({ width: authored });
1161
- text.initDimensions?.();
1162
- }
1163
- }
1164
- function asText(object) {
1165
- if (!object) return null;
1166
- const text = object;
1167
- return typeof text.text === "string" && typeof text.calcTextWidth === "function" ? text : null;
1168
- }
1169
- function textInk(text) {
1170
- const boxWidth = Math.max(0, text.width ?? 0);
1171
- const measured = text.calcTextWidth?.() ?? boxWidth;
1172
- const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));
1173
- const slack = (boxWidth - width) / 2;
1174
- const align = text.textAlign ?? "left";
1175
- if (align.includes("center")) return { width, dx: 0 };
1176
- const flip = text.flipX ? -1 : 1;
1177
- return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1178
- }
1179
- function fitTextWidth(object) {
1180
- const text = asText(object);
1181
- if (!text || text.path) return false;
1182
- const before = textInk(text);
1183
- const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1184
- if (Math.abs(fitted - text.width) < TEXT_FIT_SLACK) return false;
1185
- const centre = text.getCenterPoint();
1186
- text.set({ width: fitted });
1187
- text.initDimensions?.();
1188
- const after = textInk(text);
1189
- const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1190
- const radians = (text.angle ?? 0) * Math.PI / 180;
1191
- const moved = new Point(
1192
- centre.x + shift * Math.cos(radians),
1193
- centre.y + shift * Math.sin(radians)
1194
- );
1195
- text.setPositionByOrigin(moved, "center", "center");
1196
- text.setCoords();
1197
- text.dirty = true;
1198
- return true;
1199
- }
1164
+ import { FabricObject, Point } from "fabric";
1200
1165
 
1201
1166
  // src/pattern/tile-geometry.ts
1202
1167
  var MAX_TILES_PER_AXIS = 200;
@@ -1353,7 +1318,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1353
1318
  const centre = this.getCenterPoint();
1354
1319
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1355
1320
  const ink = this.inkOffset();
1356
- return new Point2(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1321
+ return new Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1357
1322
  }
1358
1323
  _render(ctx) {
1359
1324
  const { width, height } = this.area;
@@ -1485,12 +1450,12 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1485
1450
  */
1486
1451
  inkOffset() {
1487
1452
  const text = asText(this.source);
1488
- if (!text) return new Point2(0, 0);
1453
+ if (!text) return new Point(0, 0);
1489
1454
  const { dx } = textInk(text);
1490
- if (!dx) return new Point2(0, 0);
1455
+ if (!dx) return new Point(0, 0);
1491
1456
  const scaled = dx * (this.source.scaleX ?? 1);
1492
1457
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1493
- return new Point2(scaled * Math.cos(radians), scaled * Math.sin(radians));
1458
+ return new Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
1494
1459
  }
1495
1460
  /** The source's on-canvas width, before the tile scale. */
1496
1461
  baseW() {
@@ -1551,11 +1516,11 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1551
1516
  const radians = angle * Math.PI / 180;
1552
1517
  const cos = Math.cos(radians);
1553
1518
  const sin = Math.sin(radians);
1554
- return new Point2(dx * cos - dy * sin, dx * sin + dy * cos);
1519
+ return new Point(dx * cos - dy * sin, dx * sin + dy * cos);
1555
1520
  }
1556
1521
  anchorFromOrigin(origin, tileW, tileH, angle) {
1557
1522
  const shift = this.shiftVector(tileW, tileH, angle);
1558
- return new Point2(origin.x + shift.x, origin.y + shift.y);
1523
+ return new Point(origin.x + shift.x, origin.y + shift.y);
1559
1524
  }
1560
1525
  /**
1561
1526
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1875,7 +1840,11 @@ function normalizeTextCurve(config) {
1875
1840
  shape: config.shape ?? inferred,
1876
1841
  arc,
1877
1842
  wave,
1878
- waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
1843
+ waveLength: clamp(
1844
+ config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
1845
+ MIN_WAVE_LENGTH,
1846
+ MAX_WAVE_LENGTH
1847
+ ),
1879
1848
  offset: clamp(config.offset ?? 0, -100, 100),
1880
1849
  centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
1881
1850
  };
@@ -1965,7 +1934,7 @@ function buildCurvePathData(config, width, fontSize) {
1965
1934
  }
1966
1935
 
1967
1936
  // src/text-curve.ts
1968
- var MEASURE_WIDTH2 = 1e5;
1937
+ var MEASURE_WIDTH = 1e5;
1969
1938
  var PATH_SLACK = 0.06;
1970
1939
  var FALLBACK_LINE_HEIGHT = 1.16;
1971
1940
  var patches = /* @__PURE__ */ new WeakMap();
@@ -2013,7 +1982,7 @@ function isCurvable(object) {
2013
1982
  function measureText(text) {
2014
1983
  const authored = text.width;
2015
1984
  try {
2016
- text.set({ width: MEASURE_WIDTH2 });
1985
+ text.set({ width: MEASURE_WIDTH });
2017
1986
  text.initDimensions?.();
2018
1987
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
2019
1988
  } finally {
@@ -2483,6 +2452,7 @@ var MaskPresetManager = class {
2483
2452
  object.dirty = true;
2484
2453
  object.setCoords();
2485
2454
  this.canvas.requestRenderAll();
2455
+ this.events.emit("masks:changed", { target: layerId });
2486
2456
  this.events.emit("layer:modified", { layerId });
2487
2457
  if (save) this.history.save();
2488
2458
  return true;
@@ -2757,6 +2727,7 @@ async function deserializeEditor(editor, state) {
2757
2727
  for (const item of staged) {
2758
2728
  restoreLayer(editor, item.serialized, item.fabricObject);
2759
2729
  }
2730
+ editor.wrap.refreshAll();
2760
2731
  editor.canvas.requestRenderAll();
2761
2732
  }
2762
2733
  function restoreLayer(editor, serialized, fabricObject) {
@@ -3408,76 +3379,7 @@ function needsAbsoluteSpace(entries) {
3408
3379
  }
3409
3380
 
3410
3381
  // src/masks/edit.ts
3411
- import { util as util4 } from "fabric";
3412
-
3413
- // src/masks/space.ts
3414
3382
  import { util as util3 } from "fabric";
3415
- function matrixOf(object) {
3416
- return object.calcTransformMatrix();
3417
- }
3418
- function applyMatrix(object, matrix) {
3419
- const decomposed = util3.qrDecompose(matrix);
3420
- object.set({
3421
- flipX: false,
3422
- flipY: false,
3423
- originX: "center",
3424
- originY: "center",
3425
- left: decomposed.translateX,
3426
- top: decomposed.translateY,
3427
- scaleX: decomposed.scaleX,
3428
- scaleY: decomposed.scaleY,
3429
- angle: decomposed.angle,
3430
- skewX: decomposed.skewX,
3431
- skewY: 0
3432
- });
3433
- object.setCoords();
3434
- }
3435
- function toCanvasSpace(object, host) {
3436
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3437
- }
3438
- function toHostSpace(object, host) {
3439
- applyMatrix(
3440
- object,
3441
- util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object))
3442
- );
3443
- }
3444
- function relativeMatrix(object, host) {
3445
- return util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object));
3446
- }
3447
- function applyRelativeMatrix(object, host, rel) {
3448
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), rel));
3449
- }
3450
- function asObject(clip) {
3451
- return clip;
3452
- }
3453
- function toMatrix(values) {
3454
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3455
- return null;
3456
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3457
- }
3458
- function fitToBox(object, box, zoom = 1) {
3459
- const width = Math.max(1, box.width) * zoom;
3460
- const height = Math.max(1, box.height) * zoom;
3461
- object.set({
3462
- originX: "center",
3463
- originY: "center",
3464
- angle: 0,
3465
- skewX: 0,
3466
- skewY: 0,
3467
- left: box.left + box.width / 2,
3468
- top: box.top + box.height / 2,
3469
- scaleX: width / Math.max(1, object.width ?? 1),
3470
- scaleY: height / Math.max(1, object.height ?? 1)
3471
- });
3472
- object.setCoords();
3473
- }
3474
- function unwrapGroup(group) {
3475
- const children = group.removeAll();
3476
- for (const child of children) child.setCoords();
3477
- return children;
3478
- }
3479
-
3480
- // src/masks/edit.ts
3481
3383
  var MaskEditController = class {
3482
3384
  constructor(canvas, onCommit) {
3483
3385
  this.canvas = canvas;
@@ -3558,8 +3460,8 @@ var MaskEditController = class {
3558
3460
  if (!this.handle || !this.child || !this.group) return;
3559
3461
  applyMatrix(
3560
3462
  this.child,
3561
- util4.multiplyTransformMatrices(
3562
- util4.invertTransform(matrixOf(this.group)),
3463
+ util3.multiplyTransformMatrices(
3464
+ util3.invertTransform(matrixOf(this.group)),
3563
3465
  matrixOf(this.handle)
3564
3466
  )
3565
3467
  );
@@ -3709,6 +3611,24 @@ var MaskStackStore = class {
3709
3611
  this.pinning = false;
3710
3612
  }
3711
3613
  }
3614
+ /**
3615
+ * Is the host's own `clipPath` the wrap manager's box clip rather than a mask?
3616
+ *
3617
+ * Adoption rescues a clip whose provenance is UNKNOWN — a mask preset, or a
3618
+ * hand-set `clipPath` from before stacks existed — so the first add() cannot
3619
+ * silently throw it away. A text layer's box clip is the opposite case: its
3620
+ * provenance is known exactly (the wrap manager derives it from
3621
+ * `meta.overflow` on every pass and re-installs it under whatever owns the
3622
+ * property next), and it is not a mask. Adopted, it becomes a stack row the
3623
+ * user never made whose 'add' union covers the whole box — swallowing the mask
3624
+ * they actually added.
3625
+ *
3626
+ * Only meaningful with an empty stack: once there are entries the top-level
3627
+ * clip is the composed group and the box clip is nested underneath it.
3628
+ */
3629
+ holdsBoxClip(meta) {
3630
+ return meta?.overflow === "hidden" && !meta.maskPreset;
3631
+ }
3712
3632
  /**
3713
3633
  * Take the current geometry back out of the composed clip, entry-aligned.
3714
3634
  *
@@ -3721,7 +3641,13 @@ var MaskStackStore = class {
3721
3641
  const clip = host.clipPath;
3722
3642
  if (!clip) return [];
3723
3643
  const entries = this.list(target);
3724
- if (entries.length === 0 || !(clip instanceof Group5)) return [asObject(clip)];
3644
+ const meta = this.hostLayer(target)?.meta;
3645
+ if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
3646
+ if (entries.length === 0 || !(clip instanceof Group5)) {
3647
+ const source = asObject(clip);
3648
+ if (meta?.overflow === "hidden") source.clipPath = void 0;
3649
+ return [source];
3650
+ }
3725
3651
  const children = unwrapGroup(clip);
3726
3652
  const extra = children.length - entries.length;
3727
3653
  return extra > 0 ? children.slice(extra) : children;
@@ -4002,6 +3928,9 @@ var LayerMaskManager = class extends MaskStackStore {
4002
3928
  if (resolved === layerId) return null;
4003
3929
  const object = layer.fabricObject;
4004
3930
  if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
3931
+ const stackHost = this.host(resolved);
3932
+ const absolute = resolved === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(resolved));
3933
+ if (!absolute && stackHost) toHostSpace(object, stackHost);
4005
3934
  this.history.beginTransaction();
4006
3935
  try {
4007
3936
  if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
@@ -4049,6 +3978,7 @@ var LayerMaskManager = class extends MaskStackStore {
4049
3978
  const clip = layer?.fabricObject.clipPath;
4050
3979
  if (!layer || !clip || layer.meta.pattern) return null;
4051
3980
  if ((layer.meta.maskStack ?? []).length > 0) return null;
3981
+ if (this.holdsBoxClip(layer.meta)) return null;
4052
3982
  const entry = {
4053
3983
  ...DEFAULT_ENTRY,
4054
3984
  id: generateId(),
@@ -4101,6 +4031,7 @@ var CanvasEditor = class {
4101
4031
  crop;
4102
4032
  patterns;
4103
4033
  curves;
4034
+ wrap;
4104
4035
  maskPresets;
4105
4036
  /** Stacked boolean masks, per layer and for the design as a whole. */
4106
4037
  layerMasks;
@@ -4151,6 +4082,7 @@ var CanvasEditor = class {
4151
4082
  this.crop = new CropController(this.canvas, this.history, this.events);
4152
4083
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4153
4084
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
4085
+ this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
4154
4086
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4155
4087
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4156
4088
  this.setupCanvasEvents();
@@ -4907,15 +4839,34 @@ var CanvasEditor = class {
4907
4839
  }
4908
4840
  // ─── Text curve ─────────────────────────────────────
4909
4841
  applyTextCurve(layerId, config) {
4910
- return this.curves.apply(layerId, config);
4842
+ const applied = this.curves.apply(layerId, config);
4843
+ if (applied) this.wrap.refresh(layerId);
4844
+ return applied;
4911
4845
  }
4912
4846
  clearTextCurve(layerId) {
4913
- return this.curves.clear(layerId);
4847
+ const cleared = this.curves.clear(layerId);
4848
+ if (cleared) this.wrap.refresh(layerId);
4849
+ return cleared;
4914
4850
  }
4915
4851
  getTextCurve(layerId) {
4916
4852
  return this.curves.get(layerId);
4917
4853
  }
4854
+ // ─── Text wrap ──────────────────────────────────────
4855
+ applyTextWrap(layerId, wrap) {
4856
+ return this.wrap.apply(layerId, wrap);
4857
+ }
4858
+ setTextOverflow(layerId, overflow) {
4859
+ return this.wrap.setOverflow(layerId, overflow);
4860
+ }
4861
+ getTextWrap(layerId) {
4862
+ return this.wrap.get(layerId);
4863
+ }
4918
4864
  // ─── Mask presets ───────────────────────────────────
4865
+ // A preset assigns `clipPath` wholesale, so a text layer clipping to its box
4866
+ // loses that clip the moment a mask lands on it. Putting it back is NOT wired
4867
+ // here: the preset manager announces `masks:changed` and the wrap manager
4868
+ // re-derives from that, which covers this facade, the manager called directly,
4869
+ // and `refreshAll()` on a restore — all with one mechanism.
4919
4870
  applyMaskPreset(layerId, id) {
4920
4871
  return this.maskPresets.apply(layerId, id);
4921
4872
  }
@@ -5004,6 +4955,7 @@ var CanvasEditor = class {
5004
4955
  this.history.dispose();
5005
4956
  this.patterns.dispose();
5006
4957
  this.curves.dispose();
4958
+ this.wrap.dispose();
5007
4959
  this.events.removeAllListeners();
5008
4960
  this.canvas.dispose();
5009
4961
  }
@@ -5152,12 +5104,14 @@ export {
5152
5104
  TEXTURE_MASK_IDS,
5153
5105
  TEXTURE_MASK_SIZE,
5154
5106
  TextCurveManager,
5107
+ TextWrapManager,
5155
5108
  TiledPatternObject,
5156
5109
  UnitConverter,
5157
5110
  applyAspectLock,
5158
5111
  applyLayerShadow,
5159
5112
  applyObjectSelectionStyle,
5160
5113
  applySelectionStyle,
5114
+ applyTextWrapToObject,
5161
5115
  buildCurveLinePaths,
5162
5116
  buildCurvePathData,
5163
5117
  clamp,
@@ -5185,7 +5139,9 @@ export {
5185
5139
  isTextureMaskId,
5186
5140
  needsAbsoluteSpace,
5187
5141
  normalizeTextCurve,
5142
+ preWrapWordSplit,
5188
5143
  readLayerShadow,
5144
+ readTextWrap,
5189
5145
  renderTextureMask,
5190
5146
  resetTransform,
5191
5147
  restoreLocks,