@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.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
@@ -1969,7 +1934,7 @@ function buildCurvePathData(config, width, fontSize) {
1969
1934
  }
1970
1935
 
1971
1936
  // src/text-curve.ts
1972
- var MEASURE_WIDTH2 = 1e5;
1937
+ var MEASURE_WIDTH = 1e5;
1973
1938
  var PATH_SLACK = 0.06;
1974
1939
  var FALLBACK_LINE_HEIGHT = 1.16;
1975
1940
  var patches = /* @__PURE__ */ new WeakMap();
@@ -2017,7 +1982,7 @@ function isCurvable(object) {
2017
1982
  function measureText(text) {
2018
1983
  const authored = text.width;
2019
1984
  try {
2020
- text.set({ width: MEASURE_WIDTH2 });
1985
+ text.set({ width: MEASURE_WIDTH });
2021
1986
  text.initDimensions?.();
2022
1987
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
2023
1988
  } finally {
@@ -2487,6 +2452,7 @@ var MaskPresetManager = class {
2487
2452
  object.dirty = true;
2488
2453
  object.setCoords();
2489
2454
  this.canvas.requestRenderAll();
2455
+ this.events.emit("masks:changed", { target: layerId });
2490
2456
  this.events.emit("layer:modified", { layerId });
2491
2457
  if (save) this.history.save();
2492
2458
  return true;
@@ -2761,6 +2727,7 @@ async function deserializeEditor(editor, state) {
2761
2727
  for (const item of staged) {
2762
2728
  restoreLayer(editor, item.serialized, item.fabricObject);
2763
2729
  }
2730
+ editor.wrap.refreshAll();
2764
2731
  editor.canvas.requestRenderAll();
2765
2732
  }
2766
2733
  function restoreLayer(editor, serialized, fabricObject) {
@@ -3412,76 +3379,7 @@ function needsAbsoluteSpace(entries) {
3412
3379
  }
3413
3380
 
3414
3381
  // src/masks/edit.ts
3415
- import { util as util4 } from "fabric";
3416
-
3417
- // src/masks/space.ts
3418
3382
  import { util as util3 } from "fabric";
3419
- function matrixOf(object) {
3420
- return object.calcTransformMatrix();
3421
- }
3422
- function applyMatrix(object, matrix) {
3423
- const decomposed = util3.qrDecompose(matrix);
3424
- object.set({
3425
- flipX: false,
3426
- flipY: false,
3427
- originX: "center",
3428
- originY: "center",
3429
- left: decomposed.translateX,
3430
- top: decomposed.translateY,
3431
- scaleX: decomposed.scaleX,
3432
- scaleY: decomposed.scaleY,
3433
- angle: decomposed.angle,
3434
- skewX: decomposed.skewX,
3435
- skewY: 0
3436
- });
3437
- object.setCoords();
3438
- }
3439
- function toCanvasSpace(object, host) {
3440
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3441
- }
3442
- function toHostSpace(object, host) {
3443
- applyMatrix(
3444
- object,
3445
- util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object))
3446
- );
3447
- }
3448
- function relativeMatrix(object, host) {
3449
- return util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object));
3450
- }
3451
- function applyRelativeMatrix(object, host, rel) {
3452
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), rel));
3453
- }
3454
- function asObject(clip) {
3455
- return clip;
3456
- }
3457
- function toMatrix(values) {
3458
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3459
- return null;
3460
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3461
- }
3462
- function fitToBox(object, box, zoom = 1) {
3463
- const width = Math.max(1, box.width) * zoom;
3464
- const height = Math.max(1, box.height) * zoom;
3465
- object.set({
3466
- originX: "center",
3467
- originY: "center",
3468
- angle: 0,
3469
- skewX: 0,
3470
- skewY: 0,
3471
- left: box.left + box.width / 2,
3472
- top: box.top + box.height / 2,
3473
- scaleX: width / Math.max(1, object.width ?? 1),
3474
- scaleY: height / Math.max(1, object.height ?? 1)
3475
- });
3476
- object.setCoords();
3477
- }
3478
- function unwrapGroup(group) {
3479
- const children = group.removeAll();
3480
- for (const child of children) child.setCoords();
3481
- return children;
3482
- }
3483
-
3484
- // src/masks/edit.ts
3485
3383
  var MaskEditController = class {
3486
3384
  constructor(canvas, onCommit) {
3487
3385
  this.canvas = canvas;
@@ -3562,8 +3460,8 @@ var MaskEditController = class {
3562
3460
  if (!this.handle || !this.child || !this.group) return;
3563
3461
  applyMatrix(
3564
3462
  this.child,
3565
- util4.multiplyTransformMatrices(
3566
- util4.invertTransform(matrixOf(this.group)),
3463
+ util3.multiplyTransformMatrices(
3464
+ util3.invertTransform(matrixOf(this.group)),
3567
3465
  matrixOf(this.handle)
3568
3466
  )
3569
3467
  );
@@ -3713,6 +3611,24 @@ var MaskStackStore = class {
3713
3611
  this.pinning = false;
3714
3612
  }
3715
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
+ }
3716
3632
  /**
3717
3633
  * Take the current geometry back out of the composed clip, entry-aligned.
3718
3634
  *
@@ -3725,7 +3641,13 @@ var MaskStackStore = class {
3725
3641
  const clip = host.clipPath;
3726
3642
  if (!clip) return [];
3727
3643
  const entries = this.list(target);
3728
- 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
+ }
3729
3651
  const children = unwrapGroup(clip);
3730
3652
  const extra = children.length - entries.length;
3731
3653
  return extra > 0 ? children.slice(extra) : children;
@@ -4056,6 +3978,7 @@ var LayerMaskManager = class extends MaskStackStore {
4056
3978
  const clip = layer?.fabricObject.clipPath;
4057
3979
  if (!layer || !clip || layer.meta.pattern) return null;
4058
3980
  if ((layer.meta.maskStack ?? []).length > 0) return null;
3981
+ if (this.holdsBoxClip(layer.meta)) return null;
4059
3982
  const entry = {
4060
3983
  ...DEFAULT_ENTRY,
4061
3984
  id: generateId(),
@@ -4108,6 +4031,7 @@ var CanvasEditor = class {
4108
4031
  crop;
4109
4032
  patterns;
4110
4033
  curves;
4034
+ wrap;
4111
4035
  maskPresets;
4112
4036
  /** Stacked boolean masks, per layer and for the design as a whole. */
4113
4037
  layerMasks;
@@ -4158,6 +4082,7 @@ var CanvasEditor = class {
4158
4082
  this.crop = new CropController(this.canvas, this.history, this.events);
4159
4083
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4160
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);
4161
4086
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4162
4087
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4163
4088
  this.setupCanvasEvents();
@@ -4914,15 +4839,34 @@ var CanvasEditor = class {
4914
4839
  }
4915
4840
  // ─── Text curve ─────────────────────────────────────
4916
4841
  applyTextCurve(layerId, config) {
4917
- return this.curves.apply(layerId, config);
4842
+ const applied = this.curves.apply(layerId, config);
4843
+ if (applied) this.wrap.refresh(layerId);
4844
+ return applied;
4918
4845
  }
4919
4846
  clearTextCurve(layerId) {
4920
- return this.curves.clear(layerId);
4847
+ const cleared = this.curves.clear(layerId);
4848
+ if (cleared) this.wrap.refresh(layerId);
4849
+ return cleared;
4921
4850
  }
4922
4851
  getTextCurve(layerId) {
4923
4852
  return this.curves.get(layerId);
4924
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
+ }
4925
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.
4926
4870
  applyMaskPreset(layerId, id) {
4927
4871
  return this.maskPresets.apply(layerId, id);
4928
4872
  }
@@ -5011,6 +4955,7 @@ var CanvasEditor = class {
5011
4955
  this.history.dispose();
5012
4956
  this.patterns.dispose();
5013
4957
  this.curves.dispose();
4958
+ this.wrap.dispose();
5014
4959
  this.events.removeAllListeners();
5015
4960
  this.canvas.dispose();
5016
4961
  }
@@ -5159,12 +5104,14 @@ export {
5159
5104
  TEXTURE_MASK_IDS,
5160
5105
  TEXTURE_MASK_SIZE,
5161
5106
  TextCurveManager,
5107
+ TextWrapManager,
5162
5108
  TiledPatternObject,
5163
5109
  UnitConverter,
5164
5110
  applyAspectLock,
5165
5111
  applyLayerShadow,
5166
5112
  applyObjectSelectionStyle,
5167
5113
  applySelectionStyle,
5114
+ applyTextWrapToObject,
5168
5115
  buildCurveLinePaths,
5169
5116
  buildCurvePathData,
5170
5117
  clamp,
@@ -5192,7 +5139,9 @@ export {
5192
5139
  isTextureMaskId,
5193
5140
  needsAbsoluteSpace,
5194
5141
  normalizeTextCurve,
5142
+ preWrapWordSplit,
5195
5143
  readLayerShadow,
5144
+ readTextWrap,
5196
5145
  renderTextureMask,
5197
5146
  resetTransform,
5198
5147
  restoreLocks,