@overtone-art/canvas-editor-core 0.5.1 → 0.6.1
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.d.mts +146 -15
- package/dist/index.d.ts +146 -15
- package/dist/index.global.js +52 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +633 -167
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +584 -123
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-JP8MZsIs.d.mts → types-LM7pOkkj.d.mts} +34 -4
- package/dist/{types-JP8MZsIs.d.ts → types-LM7pOkkj.d.ts} +34 -4
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./chunk-MCBRZQ4M.mjs";
|
|
12
12
|
|
|
13
13
|
// src/editor.ts
|
|
14
|
-
import { Canvas, FabricImage as FabricImage3, Group as
|
|
14
|
+
import { Canvas, FabricImage as FabricImage3, Group as Group7, Textbox as Textbox2, filters } from "fabric";
|
|
15
15
|
|
|
16
16
|
// src/events.ts
|
|
17
17
|
var EventEmitter = class {
|
|
@@ -48,6 +48,9 @@ var EventEmitter = class {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// src/layer.ts
|
|
52
|
+
import { ActiveSelection } from "fabric";
|
|
53
|
+
|
|
51
54
|
// src/utils/id.ts
|
|
52
55
|
import { nanoid } from "nanoid";
|
|
53
56
|
var generateId = () => nanoid(12);
|
|
@@ -69,6 +72,14 @@ var Layer = class {
|
|
|
69
72
|
* the layer's real object stays the one every editor API talks to.
|
|
70
73
|
*/
|
|
71
74
|
renderProxy;
|
|
75
|
+
/**
|
|
76
|
+
* Layers nested inside this one, in draw order. Only a `group` layer has any:
|
|
77
|
+
* their fabric objects live inside this layer's fabric `Group`, not on the
|
|
78
|
+
* canvas, which is why the manager keeps them off the canvas stack.
|
|
79
|
+
*/
|
|
80
|
+
children;
|
|
81
|
+
/** Group this layer sits inside, or null when it is on the canvas stack. */
|
|
82
|
+
parentId;
|
|
72
83
|
constructor(type, fabricObject, name, id) {
|
|
73
84
|
this.id = id ?? generateId();
|
|
74
85
|
this.type = type;
|
|
@@ -79,6 +90,8 @@ var Layer = class {
|
|
|
79
90
|
this.meta = {};
|
|
80
91
|
this.fabricObject = fabricObject;
|
|
81
92
|
this.renderProxy = null;
|
|
93
|
+
this.children = [];
|
|
94
|
+
this.parentId = null;
|
|
82
95
|
this.fabricObject._layerId = this.id;
|
|
83
96
|
}
|
|
84
97
|
hasMeta() {
|
|
@@ -92,12 +105,15 @@ var Layer = class {
|
|
|
92
105
|
visible: this.visible,
|
|
93
106
|
locked: this.locked,
|
|
94
107
|
opacity: this.opacity,
|
|
95
|
-
...this.hasMeta() ? { meta: this.meta } : {}
|
|
108
|
+
...this.hasMeta() ? { meta: this.meta } : {},
|
|
109
|
+
...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
|
|
96
110
|
};
|
|
97
111
|
}
|
|
98
112
|
serialize() {
|
|
99
113
|
return {
|
|
100
114
|
...this.toData(),
|
|
115
|
+
// A group's own object already carries its children's geometry, so the
|
|
116
|
+
// child records in `toData` stay identity-only — nothing is duplicated.
|
|
101
117
|
fabricObject: this.fabricObject.toObject()
|
|
102
118
|
};
|
|
103
119
|
}
|
|
@@ -123,9 +139,23 @@ var LayerManager = class {
|
|
|
123
139
|
return layer;
|
|
124
140
|
}
|
|
125
141
|
remove(id) {
|
|
142
|
+
const layer = this.get(id);
|
|
143
|
+
if (!layer) return false;
|
|
144
|
+
if (layer.parentId) {
|
|
145
|
+
const parent = this.get(layer.parentId);
|
|
146
|
+
if (!parent) return false;
|
|
147
|
+
parent.fabricObject.remove(layer.fabricObject);
|
|
148
|
+
parent.children = parent.children.filter((c) => c.id !== id);
|
|
149
|
+
layer.parentId = null;
|
|
150
|
+
this.events.emit("layer:removed", { layerId: id });
|
|
151
|
+
if (parent.children.length < 2) this.dissolve(parent);
|
|
152
|
+
this.emitChanged();
|
|
153
|
+
this.onPropertyChanged?.();
|
|
154
|
+
this.canvas.requestRenderAll();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
126
157
|
const index = this.layers.findIndex((l) => l.id === id);
|
|
127
158
|
if (index === -1) return false;
|
|
128
|
-
const layer = this.layers[index];
|
|
129
159
|
this.canvas.remove(layer.fabricObject);
|
|
130
160
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
131
161
|
this.layers.splice(index, 1);
|
|
@@ -134,6 +164,81 @@ var LayerManager = class {
|
|
|
134
164
|
this.onPropertyChanged?.();
|
|
135
165
|
return true;
|
|
136
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Take a top-level layer off the canvas stack **without** disposing it — it is
|
|
169
|
+
* about to become a group's child, so its object has to stay alive.
|
|
170
|
+
*/
|
|
171
|
+
detach(id) {
|
|
172
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
173
|
+
if (index === -1) return void 0;
|
|
174
|
+
const [layer] = this.layers.splice(index, 1);
|
|
175
|
+
this.canvas.remove(layer.fabricObject);
|
|
176
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
177
|
+
return layer;
|
|
178
|
+
}
|
|
179
|
+
/** Put a group's children back on the canvas stack and drop the group. */
|
|
180
|
+
explode(group) {
|
|
181
|
+
const children = [...group.children];
|
|
182
|
+
this.dissolve(group);
|
|
183
|
+
this.events.emit("layer:removed", { layerId: group.id });
|
|
184
|
+
this.emitChanged();
|
|
185
|
+
this.onPropertyChanged?.();
|
|
186
|
+
this.canvas.requestRenderAll();
|
|
187
|
+
return children;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Empty a group onto the canvas stack, in the group's own place, and drop the
|
|
191
|
+
* group record. `removeAll` restores each child's absolute transform, so the
|
|
192
|
+
* group's matrix must NOT be re-applied on top of it.
|
|
193
|
+
*/
|
|
194
|
+
dissolve(group) {
|
|
195
|
+
const objects = group.fabricObject.removeAll();
|
|
196
|
+
const at = this.layers.findIndex((l) => l.id === group.id);
|
|
197
|
+
const survivors = group.children;
|
|
198
|
+
group.children = [];
|
|
199
|
+
for (const [index, object] of objects.entries()) {
|
|
200
|
+
object.setCoords();
|
|
201
|
+
const child = survivors[index];
|
|
202
|
+
if (!child) continue;
|
|
203
|
+
child.parentId = null;
|
|
204
|
+
this.layers.splice(at + index, 0, child);
|
|
205
|
+
this.canvas.add(object);
|
|
206
|
+
if (child.renderProxy) this.canvas.add(child.renderProxy);
|
|
207
|
+
}
|
|
208
|
+
this.canvas.remove(group.fabricObject);
|
|
209
|
+
this.layers = this.layers.filter((l) => l.id !== group.id);
|
|
210
|
+
this.syncZOrder();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Re-attach child records to an enlivened group. `data[i]` describes
|
|
214
|
+
* `group._objects[i]` — the order fabric serializes and restores them in. A
|
|
215
|
+
* missing record still becomes a child, so a hand-edited state cannot drop
|
|
216
|
+
* artwork off the canvas.
|
|
217
|
+
*/
|
|
218
|
+
adoptGroupChildren(group, data) {
|
|
219
|
+
const objects = group.fabricObject.getObjects();
|
|
220
|
+
group.children = objects.map((object, index) => {
|
|
221
|
+
const record = data[index];
|
|
222
|
+
const child = new Layer(record?.type ?? "shape", object, record?.name, record?.id);
|
|
223
|
+
child.parentId = group.id;
|
|
224
|
+
if (record?.meta) child.meta = structuredClone(record.meta);
|
|
225
|
+
if (record && !record.visible) {
|
|
226
|
+
child.visible = false;
|
|
227
|
+
object.visible = false;
|
|
228
|
+
}
|
|
229
|
+
if (record?.locked) {
|
|
230
|
+
child.locked = true;
|
|
231
|
+
object.selectable = false;
|
|
232
|
+
object.evented = false;
|
|
233
|
+
}
|
|
234
|
+
if (record && record.opacity !== 1) {
|
|
235
|
+
child.opacity = record.opacity;
|
|
236
|
+
object.opacity = record.opacity;
|
|
237
|
+
}
|
|
238
|
+
return child;
|
|
239
|
+
});
|
|
240
|
+
this.emitChanged();
|
|
241
|
+
}
|
|
137
242
|
/** Replace a layer's render object while preserving its immutable ID and panel state. */
|
|
138
243
|
replaceObject(id, fabricObject) {
|
|
139
244
|
const layer = this.get(id);
|
|
@@ -198,16 +303,32 @@ var LayerManager = class {
|
|
|
198
303
|
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
199
304
|
}
|
|
200
305
|
}
|
|
306
|
+
/** The list a layer lives in: its group's children, or the canvas stack. */
|
|
307
|
+
siblingsOf(layer) {
|
|
308
|
+
if (!layer.parentId) return this.layers;
|
|
309
|
+
return this.get(layer.parentId)?.children ?? this.layers;
|
|
310
|
+
}
|
|
201
311
|
reorder(id, newIndex) {
|
|
202
|
-
const
|
|
312
|
+
const target = this.get(id);
|
|
313
|
+
if (!target) return false;
|
|
314
|
+
const list = this.siblingsOf(target);
|
|
315
|
+
const oldIndex = list.indexOf(target);
|
|
203
316
|
if (oldIndex === -1) return false;
|
|
204
317
|
if (!Number.isFinite(newIndex)) return false;
|
|
205
|
-
const clamped = Math.max(0, Math.min(
|
|
318
|
+
const clamped = Math.max(0, Math.min(list.length - 1, Math.round(newIndex)));
|
|
206
319
|
if (oldIndex === clamped) return false;
|
|
207
|
-
const [layer] =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
320
|
+
const [layer] = list.splice(oldIndex, 1);
|
|
321
|
+
list.splice(clamped, 0, layer);
|
|
322
|
+
if (layer.parentId) {
|
|
323
|
+
const group = this.get(layer.parentId)?.fabricObject;
|
|
324
|
+
if (group) {
|
|
325
|
+
group.remove(layer.fabricObject);
|
|
326
|
+
group.insertAt(clamped, layer.fabricObject);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
this.syncZOrder();
|
|
330
|
+
}
|
|
331
|
+
this.events.emit("layer:reordered", { layerIds: list.map((l) => l.id) });
|
|
211
332
|
this.emitChanged();
|
|
212
333
|
this.onPropertyChanged?.();
|
|
213
334
|
return true;
|
|
@@ -215,9 +336,12 @@ var LayerManager = class {
|
|
|
215
336
|
select(id) {
|
|
216
337
|
if (id === null) {
|
|
217
338
|
this.canvas.discardActiveObject();
|
|
339
|
+
this.exitGroups();
|
|
218
340
|
} else {
|
|
219
341
|
const layer = this.get(id);
|
|
220
342
|
if (layer) {
|
|
343
|
+
if (layer.parentId) this.enterGroup(layer.parentId);
|
|
344
|
+
else this.exitGroups();
|
|
221
345
|
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
222
346
|
}
|
|
223
347
|
}
|
|
@@ -226,16 +350,67 @@ var LayerManager = class {
|
|
|
226
350
|
const selected = id && this.get(id) ? [id] : [];
|
|
227
351
|
this.events.emit("selection:changed", { selected });
|
|
228
352
|
}
|
|
353
|
+
/** Drill into a group so its children can be selected and clicked. */
|
|
354
|
+
enterGroup(groupId) {
|
|
355
|
+
const parent = this.get(groupId);
|
|
356
|
+
if (!parent) return;
|
|
357
|
+
const group = parent.fabricObject;
|
|
358
|
+
group.subTargetCheck = true;
|
|
359
|
+
group.interactive = true;
|
|
360
|
+
}
|
|
361
|
+
/** Leave every entered group — selecting anything else is done drilling in. */
|
|
362
|
+
exitGroups() {
|
|
363
|
+
for (const layer of this.layers) {
|
|
364
|
+
if (layer.type !== "group") continue;
|
|
365
|
+
const group = layer.fabricObject;
|
|
366
|
+
if (group.interactive) group.interactive = false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Select several layers at once, as one fabric `ActiveSelection`. Children are
|
|
371
|
+
* skipped: a selection spanning in and out of a group has no single transform.
|
|
372
|
+
*/
|
|
373
|
+
selectMany(ids) {
|
|
374
|
+
const layers = ids.flatMap((id) => {
|
|
375
|
+
const layer = this.get(id);
|
|
376
|
+
return layer && !layer.parentId ? [layer] : [];
|
|
377
|
+
});
|
|
378
|
+
if (layers.length === 0) return this.select(null);
|
|
379
|
+
if (layers.length === 1) return this.select(layers[0].id);
|
|
380
|
+
this.canvas.discardActiveObject();
|
|
381
|
+
this.exitGroups();
|
|
382
|
+
const selection = new ActiveSelection(
|
|
383
|
+
layers.map((l) => l.renderProxy ?? l.fabricObject),
|
|
384
|
+
{ canvas: this.canvas }
|
|
385
|
+
);
|
|
386
|
+
this.canvas.setActiveObject(selection);
|
|
387
|
+
this.canvas.requestRenderAll();
|
|
388
|
+
this.events.emit("selection:changed", { selected: layers.map((l) => l.id) });
|
|
389
|
+
}
|
|
390
|
+
/** Depth-first over the whole tree, each layer before its children. */
|
|
391
|
+
*walk(layers = this.layers) {
|
|
392
|
+
for (const layer of layers) {
|
|
393
|
+
yield layer;
|
|
394
|
+
if (layer.children.length > 0) yield* this.walk(layer.children);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
229
397
|
get(id) {
|
|
230
|
-
|
|
398
|
+
for (const layer of this.walk()) if (layer.id === id) return layer;
|
|
399
|
+
return void 0;
|
|
231
400
|
}
|
|
401
|
+
/** Top-level layers only — the canvas stack. Children hang off their group. */
|
|
232
402
|
getAll() {
|
|
233
403
|
return [...this.layers];
|
|
234
404
|
}
|
|
405
|
+
/** Top-level layers, each carrying its children: what the layer panel renders. */
|
|
406
|
+
getTree() {
|
|
407
|
+
return this.layers.map((l) => l.toData());
|
|
408
|
+
}
|
|
235
409
|
findByObject(obj) {
|
|
236
410
|
const layerId = obj._layerId;
|
|
237
411
|
if (layerId) return this.get(layerId);
|
|
238
|
-
|
|
412
|
+
for (const layer of this.walk()) if (layer.fabricObject === obj) return layer;
|
|
413
|
+
return void 0;
|
|
239
414
|
}
|
|
240
415
|
setVisibility(id, visible) {
|
|
241
416
|
const layer = this.get(id);
|
|
@@ -316,6 +491,7 @@ var LayerManager = class {
|
|
|
316
491
|
for (const layer of this.layers) {
|
|
317
492
|
this.canvas.remove(layer.fabricObject);
|
|
318
493
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
494
|
+
layer.children = [];
|
|
319
495
|
}
|
|
320
496
|
this.layers = [];
|
|
321
497
|
this.emitChanged();
|
|
@@ -330,6 +506,107 @@ var LayerManager = class {
|
|
|
330
506
|
}
|
|
331
507
|
};
|
|
332
508
|
|
|
509
|
+
// src/template-explode.ts
|
|
510
|
+
import { Textbox, loadSVGFromString } from "fabric";
|
|
511
|
+
var NAME_MAX = 24;
|
|
512
|
+
function isText(object) {
|
|
513
|
+
return typeof object.text === "string";
|
|
514
|
+
}
|
|
515
|
+
function labelsByRun(template, values) {
|
|
516
|
+
const labels = new Map(template.parameters.map((p) => [p.key, p.label || p.key]));
|
|
517
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const match of template.svg.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g)) {
|
|
519
|
+
const token = /\{\{(\w+)(?:[|:]([^}]*))?\}\}/.exec(match[1]);
|
|
520
|
+
if (!token) continue;
|
|
521
|
+
const label = labels.get(token[1]);
|
|
522
|
+
if (!label) continue;
|
|
523
|
+
const resolved = String(values[token[1]] ?? token[2] ?? "").trim();
|
|
524
|
+
if (resolved) byRun.set(resolved, label);
|
|
525
|
+
}
|
|
526
|
+
return byRun;
|
|
527
|
+
}
|
|
528
|
+
function textName(run, labels) {
|
|
529
|
+
const trimmed = run.trim();
|
|
530
|
+
const label = labels.get(trimmed);
|
|
531
|
+
if (label) return label;
|
|
532
|
+
if (!trimmed) return "Text";
|
|
533
|
+
return trimmed.length > NAME_MAX ? `${trimmed.slice(0, NAME_MAX - 1)}\u2026` : trimmed;
|
|
534
|
+
}
|
|
535
|
+
function shapeName(object) {
|
|
536
|
+
const id = object.id;
|
|
537
|
+
if (typeof id === "string" && id) return id;
|
|
538
|
+
const type = object.type || "shape";
|
|
539
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
540
|
+
}
|
|
541
|
+
function toTextbox(text) {
|
|
542
|
+
const box = new Textbox(text.text, {
|
|
543
|
+
left: text.left,
|
|
544
|
+
top: text.top,
|
|
545
|
+
originX: text.originX,
|
|
546
|
+
originY: text.originY,
|
|
547
|
+
angle: text.angle,
|
|
548
|
+
scaleX: text.scaleX,
|
|
549
|
+
scaleY: text.scaleY,
|
|
550
|
+
fontSize: text.fontSize,
|
|
551
|
+
fontFamily: text.fontFamily,
|
|
552
|
+
fontWeight: text.fontWeight,
|
|
553
|
+
fontStyle: text.fontStyle,
|
|
554
|
+
textAlign: text.textAlign,
|
|
555
|
+
charSpacing: text.charSpacing,
|
|
556
|
+
lineHeight: text.lineHeight,
|
|
557
|
+
fill: text.fill,
|
|
558
|
+
stroke: text.stroke,
|
|
559
|
+
strokeWidth: text.strokeWidth,
|
|
560
|
+
opacity: text.opacity,
|
|
561
|
+
// A Textbox wraps at its box width; size it to the run the template drew so
|
|
562
|
+
// the authored copy never comes in pre-wrapped.
|
|
563
|
+
width: Math.max(1, (text.width ?? 0) + 2)
|
|
564
|
+
});
|
|
565
|
+
return box;
|
|
566
|
+
}
|
|
567
|
+
function textPathCurve(template, values, runWidth) {
|
|
568
|
+
if (template.id === "circular-text") return { arc: 100, wave: 0 };
|
|
569
|
+
if (template.id === "wave-text") return { arc: 0, wave: 50 };
|
|
570
|
+
const radius = Number(values.radius ?? 200);
|
|
571
|
+
if (!Number.isFinite(radius) || radius <= 0) return { arc: 0, wave: 0 };
|
|
572
|
+
const arc = 100 * runWidth / (Math.PI * radius);
|
|
573
|
+
return { arc: Math.max(-100, Math.min(100, arc)), wave: 0 };
|
|
574
|
+
}
|
|
575
|
+
function textPathSpec(template, values) {
|
|
576
|
+
const content = String(values.text ?? "Text");
|
|
577
|
+
const fontSize = template.id === "circular-text" ? 28 : 40;
|
|
578
|
+
const box = new Textbox(content, {
|
|
579
|
+
fontSize,
|
|
580
|
+
fontFamily: String(values.font ?? "Arial"),
|
|
581
|
+
fontWeight: "bold",
|
|
582
|
+
fill: String(values.color ?? "#000000"),
|
|
583
|
+
originX: "left",
|
|
584
|
+
originY: "top",
|
|
585
|
+
// Wide enough that the run stays on one line before the curve widens it.
|
|
586
|
+
width: Math.max(1, content.length * fontSize * 0.62)
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
type: "text",
|
|
590
|
+
name: template.parameters.find((p) => p.key === "text")?.label || "Text",
|
|
591
|
+
object: box,
|
|
592
|
+
curve: textPathCurve(template, values, box.width ?? 0)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function explodeTemplateSvg(template, resolvedSvg, values) {
|
|
596
|
+
const labels = labelsByRun(template, values);
|
|
597
|
+
const { objects } = await loadSVGFromString(resolvedSvg);
|
|
598
|
+
const specs = [];
|
|
599
|
+
for (const object of objects) {
|
|
600
|
+
if (object === null) continue;
|
|
601
|
+
if (isText(object)) {
|
|
602
|
+
specs.push({ type: "text", name: textName(object.text, labels), object: toTextbox(object) });
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
specs.push({ type: "shape", name: shapeName(object), object });
|
|
606
|
+
}
|
|
607
|
+
return specs;
|
|
608
|
+
}
|
|
609
|
+
|
|
333
610
|
// src/history.ts
|
|
334
611
|
var HistoryManager = class _HistoryManager {
|
|
335
612
|
static ASSET_KEY = "__canvasEditorHistoryAsset";
|
|
@@ -1463,90 +1740,153 @@ function restoreLocks(obj, locks) {
|
|
|
1463
1740
|
|
|
1464
1741
|
// src/text-curve.ts
|
|
1465
1742
|
import { Path } from "fabric";
|
|
1466
|
-
|
|
1743
|
+
|
|
1744
|
+
// src/text-curve-geometry.ts
|
|
1745
|
+
var DEFAULT_TEXT_CURVE = {
|
|
1746
|
+
shape: "arc",
|
|
1747
|
+
arc: 0,
|
|
1748
|
+
wave: 0,
|
|
1749
|
+
waveLength: 4.1,
|
|
1750
|
+
offset: 0,
|
|
1751
|
+
centerOffset: 0
|
|
1752
|
+
};
|
|
1467
1753
|
var MIN_ARC = 0.5;
|
|
1468
1754
|
var FULL_CIRCLE_ARC = 99.5;
|
|
1469
1755
|
var MIN_SWEEP = 0.12;
|
|
1470
|
-
var WAVE_PERIOD_EM = 4.1;
|
|
1471
1756
|
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1757
|
+
var MIN_WAVE_LENGTH = 1;
|
|
1758
|
+
var MAX_WAVE_LENGTH = 12;
|
|
1472
1759
|
var WAVE_STEP = 6;
|
|
1473
|
-
var
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
return !!object && typeof object.text === "string";
|
|
1760
|
+
var MAX_WAVE_STEPS = 4e3;
|
|
1761
|
+
function round(value) {
|
|
1762
|
+
return Math.round(value * 100) / 100;
|
|
1477
1763
|
}
|
|
1478
|
-
function
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1764
|
+
function normalizeTextCurve(config) {
|
|
1765
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
1766
|
+
const wave = clamp(config.wave ?? 0, 0, 100);
|
|
1767
|
+
const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
|
|
1768
|
+
return {
|
|
1769
|
+
shape: config.shape ?? inferred,
|
|
1770
|
+
arc,
|
|
1771
|
+
wave,
|
|
1772
|
+
waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
|
|
1773
|
+
offset: clamp(config.offset ?? 0, -100, 100),
|
|
1774
|
+
centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
function bendsBaseline(config) {
|
|
1778
|
+
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
1779
|
+
}
|
|
1780
|
+
function arcGeometry(width, config) {
|
|
1781
|
+
const magnitude = Math.min(100, Math.abs(config.arc));
|
|
1782
|
+
const direction = config.arc < 0 ? -1 : 1;
|
|
1783
|
+
const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1784
|
+
const baseRadius = width / baseSweep;
|
|
1785
|
+
const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
|
|
1786
|
+
const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
|
|
1787
|
+
return { radius, sweep, direction, baseRadius };
|
|
1488
1788
|
}
|
|
1489
|
-
function
|
|
1490
|
-
const
|
|
1491
|
-
const
|
|
1492
|
-
const full = magnitude >= FULL_CIRCLE_ARC;
|
|
1493
|
-
const sweep = full ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1494
|
-
const radius = width / sweep;
|
|
1789
|
+
function arcLinePath(width, geometry, radius, rotation) {
|
|
1790
|
+
const { sweep, direction, baseRadius } = geometry;
|
|
1791
|
+
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
1495
1792
|
const centerX = width / 2;
|
|
1496
1793
|
const point = (angle) => [
|
|
1497
1794
|
centerX + radius * Math.sin(angle),
|
|
1498
|
-
direction * radius *
|
|
1795
|
+
direction * (baseRadius - radius * Math.cos(angle))
|
|
1499
1796
|
];
|
|
1500
1797
|
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1501
1798
|
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1799
|
+
const r = round(radius);
|
|
1502
1800
|
if (full) {
|
|
1503
|
-
const start2 = point(-Math.PI);
|
|
1504
|
-
const
|
|
1801
|
+
const start2 = point(rotation - Math.PI);
|
|
1802
|
+
const opposite = point(rotation);
|
|
1505
1803
|
return {
|
|
1506
1804
|
data: [
|
|
1507
1805
|
`M ${format(start2)}`,
|
|
1508
|
-
`A ${
|
|
1509
|
-
`A ${
|
|
1806
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
1807
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1510
1808
|
].join(" "),
|
|
1511
|
-
length:
|
|
1809
|
+
length: radius * Math.PI * 2
|
|
1512
1810
|
};
|
|
1513
1811
|
}
|
|
1514
|
-
const start = point(-sweep / 2);
|
|
1515
|
-
const end = point(sweep / 2);
|
|
1812
|
+
const start = point(rotation - sweep / 2);
|
|
1813
|
+
const end = point(rotation + sweep / 2);
|
|
1516
1814
|
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1517
1815
|
return {
|
|
1518
|
-
data: `M ${format(start)} A ${
|
|
1519
|
-
|
|
1520
|
-
length: width
|
|
1816
|
+
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1817
|
+
length: radius * sweep
|
|
1521
1818
|
};
|
|
1522
1819
|
}
|
|
1523
|
-
function
|
|
1524
|
-
const amplitude =
|
|
1525
|
-
const period = Math.max(1, fontSize *
|
|
1526
|
-
const
|
|
1820
|
+
function waveLinePath(width, fontSize, config, dy) {
|
|
1821
|
+
const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
1822
|
+
const period = Math.max(1, fontSize * config.waveLength);
|
|
1823
|
+
const phase = config.offset / 100 * Math.PI * 2;
|
|
1824
|
+
const step = Math.min(WAVE_STEP, period / 8);
|
|
1825
|
+
const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
|
|
1527
1826
|
const commands = [];
|
|
1528
1827
|
let length = 0;
|
|
1529
1828
|
let previous = null;
|
|
1530
1829
|
for (let index = 0; index <= steps; index++) {
|
|
1531
1830
|
const x = width * index / steps;
|
|
1532
|
-
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1831
|
+
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
1533
1832
|
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1534
1833
|
previous = [x, y];
|
|
1535
1834
|
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1536
1835
|
}
|
|
1537
1836
|
return { data: commands.join(" "), length };
|
|
1538
1837
|
}
|
|
1539
|
-
function
|
|
1540
|
-
|
|
1838
|
+
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
1839
|
+
const curve = normalizeTextCurve(config);
|
|
1840
|
+
if (!bendsBaseline(curve)) return null;
|
|
1841
|
+
const lines = Math.max(1, Math.floor(lineCount));
|
|
1842
|
+
const paths = [];
|
|
1843
|
+
if (curve.shape === "wave") {
|
|
1844
|
+
for (let index = 0; index < lines; index++) {
|
|
1845
|
+
paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
1846
|
+
}
|
|
1847
|
+
return paths;
|
|
1848
|
+
}
|
|
1849
|
+
const geometry = arcGeometry(width, curve);
|
|
1850
|
+
const rotation = curve.offset / 100 * geometry.sweep;
|
|
1851
|
+
for (let index = 0; index < lines; index++) {
|
|
1852
|
+
const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
|
|
1853
|
+
paths.push(arcLinePath(width, geometry, radius, rotation));
|
|
1854
|
+
}
|
|
1855
|
+
return paths;
|
|
1541
1856
|
}
|
|
1542
1857
|
function buildCurvePathData(config, width, fontSize) {
|
|
1543
|
-
|
|
1544
|
-
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1545
|
-
return null;
|
|
1858
|
+
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
1546
1859
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1860
|
+
|
|
1861
|
+
// src/text-curve.ts
|
|
1862
|
+
var MEASURE_WIDTH = 1e5;
|
|
1863
|
+
var PATH_SLACK = 0.06;
|
|
1864
|
+
var FALLBACK_LINE_HEIGHT = 1.16;
|
|
1865
|
+
var patches = /* @__PURE__ */ new WeakMap();
|
|
1866
|
+
function isCurvable(object) {
|
|
1867
|
+
return !!object && typeof object.text === "string";
|
|
1868
|
+
}
|
|
1869
|
+
function measureText(text) {
|
|
1870
|
+
const authored = text.width;
|
|
1871
|
+
try {
|
|
1872
|
+
text.set({ width: MEASURE_WIDTH });
|
|
1873
|
+
text.initDimensions?.();
|
|
1874
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
1875
|
+
} finally {
|
|
1876
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
1877
|
+
text.initDimensions?.();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function lineCountOf(text) {
|
|
1881
|
+
return Math.max(1, text._textLines?.length ?? 1);
|
|
1882
|
+
}
|
|
1883
|
+
function lineHeightOf(text) {
|
|
1884
|
+
const measured = text.getHeightOfLine?.(0);
|
|
1885
|
+
if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
|
|
1886
|
+
return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
|
|
1887
|
+
}
|
|
1888
|
+
function toFabricPath(curve) {
|
|
1889
|
+
return new Path(curve.data, { visible: false, objectCaching: false });
|
|
1550
1890
|
}
|
|
1551
1891
|
var TextCurveManager = class {
|
|
1552
1892
|
constructor(canvas, layers, history, events) {
|
|
@@ -1563,33 +1903,35 @@ var TextCurveManager = class {
|
|
|
1563
1903
|
get(layerId) {
|
|
1564
1904
|
const layer = this.layers.get(layerId);
|
|
1565
1905
|
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1566
|
-
return layer.meta.curve ?? {
|
|
1906
|
+
return normalizeTextCurve(layer.meta.curve ?? {});
|
|
1567
1907
|
}
|
|
1568
1908
|
isCurved(layerId) {
|
|
1569
1909
|
const curve = this.get(layerId);
|
|
1570
|
-
return !!curve && (
|
|
1910
|
+
return !!curve && bendsBaseline(curve);
|
|
1571
1911
|
}
|
|
1572
|
-
/** Apply (or update) the curve on a text layer.
|
|
1912
|
+
/** Apply (or update) the curve on a text layer. A straight config clears it. */
|
|
1573
1913
|
apply(layerId, config, save = true) {
|
|
1574
1914
|
const layer = this.layers.get(layerId);
|
|
1575
1915
|
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1576
|
-
const next =
|
|
1916
|
+
const next = normalizeTextCurve(config);
|
|
1577
1917
|
const text = layer.fabricObject;
|
|
1578
1918
|
const run = measureText(text);
|
|
1579
|
-
|
|
1580
|
-
if (!curve) {
|
|
1919
|
+
if (!bendsBaseline(next)) {
|
|
1581
1920
|
this.detach(text, layer.meta.curveWidth);
|
|
1582
|
-
delete layer.meta.curve;
|
|
1583
1921
|
delete layer.meta.curveWidth;
|
|
1922
|
+
layer.meta.curve = next;
|
|
1584
1923
|
} else {
|
|
1585
1924
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1586
1925
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1587
|
-
text.
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1926
|
+
text.initDimensions?.();
|
|
1927
|
+
const curves = buildCurveLinePaths(
|
|
1928
|
+
next,
|
|
1929
|
+
run * (1 + PATH_SLACK),
|
|
1930
|
+
text.fontSize,
|
|
1931
|
+
lineCountOf(text),
|
|
1932
|
+
lineHeightOf(text)
|
|
1933
|
+
);
|
|
1934
|
+
if (curves) this.attach(text, curves);
|
|
1593
1935
|
layer.meta.curve = next;
|
|
1594
1936
|
}
|
|
1595
1937
|
text.initDimensions?.();
|
|
@@ -1602,25 +1944,107 @@ var TextCurveManager = class {
|
|
|
1602
1944
|
}
|
|
1603
1945
|
/** Remove the curve, restoring the authored text box width. */
|
|
1604
1946
|
clear(layerId, save = true) {
|
|
1605
|
-
|
|
1947
|
+
const layer = this.layers.get(layerId);
|
|
1948
|
+
const shape = layer?.meta.curve?.shape;
|
|
1949
|
+
return this.apply(layerId, { shape }, save);
|
|
1606
1950
|
}
|
|
1607
1951
|
/**
|
|
1608
|
-
* Rebuild the
|
|
1609
|
-
* font size all change the
|
|
1610
|
-
* without this the curve keeps
|
|
1952
|
+
* Rebuild the paths from the stored parameters. Text content, line count,
|
|
1953
|
+
* font family and font size all change the geometry the paths are sized to —
|
|
1954
|
+
* without this the curve keeps that of the text it was created from.
|
|
1611
1955
|
*/
|
|
1612
1956
|
refresh(layerId, save = false) {
|
|
1613
1957
|
const curve = this.layers.get(layerId)?.meta.curve;
|
|
1614
1958
|
if (!curve) return false;
|
|
1615
1959
|
return this.apply(layerId, curve, save);
|
|
1616
1960
|
}
|
|
1617
|
-
/**
|
|
1961
|
+
/**
|
|
1962
|
+
* Rebuild every curved layer — used after a state restore.
|
|
1963
|
+
*
|
|
1964
|
+
* `getAll` is the canvas stack, so this walks each group's children too:
|
|
1965
|
+
* curved text inside a group would otherwise keep the path geometry it was
|
|
1966
|
+
* restored with, and stop tracking its own text, font and size.
|
|
1967
|
+
*/
|
|
1618
1968
|
refreshAll() {
|
|
1619
|
-
|
|
1620
|
-
|
|
1969
|
+
const visit = (layers) => {
|
|
1970
|
+
for (const layer of layers) {
|
|
1971
|
+
if (layer.meta.curve) this.refresh(layer.id);
|
|
1972
|
+
if (layer.children.length > 0) visit(layer.children);
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
visit(this.layers.getAll());
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Puts every line's path on the object as one shape, and hooks per-line
|
|
1979
|
+
* measuring so each line is laid out on its own.
|
|
1980
|
+
*
|
|
1981
|
+
* The object's own path is the union of the lines: fabric sizes a curved text
|
|
1982
|
+
* object from it, and a single line's path would leave the rest of the block
|
|
1983
|
+
* outside the layer's box — where object caching clips it away.
|
|
1984
|
+
*/
|
|
1985
|
+
attach(text, curves) {
|
|
1986
|
+
const paths = curves.map(toFabricPath);
|
|
1987
|
+
const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
|
|
1988
|
+
text.set({
|
|
1989
|
+
path: union,
|
|
1990
|
+
pathAlign: "center",
|
|
1991
|
+
pathSide: "left",
|
|
1992
|
+
// Each line is centred on its own path by the measuring hook below, so
|
|
1993
|
+
// there is no single offset left to apply.
|
|
1994
|
+
pathStartOffset: 0
|
|
1995
|
+
});
|
|
1996
|
+
this.patchLineMeasure(text, paths, union);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Fabric lays every line of a text object along `this.path`, from one
|
|
2000
|
+
* `pathStartOffset` — so a multi-line run piles all its lines on top of each
|
|
2001
|
+
* other. Each line is measured against its own path instead, by swapping the
|
|
2002
|
+
* path in around fabric's own per-line measure pass.
|
|
2003
|
+
*
|
|
2004
|
+
* The paths all share the first one's `pathOffset`, which is the origin
|
|
2005
|
+
* fabric subtracts to place a glyph in the object — without that each line
|
|
2006
|
+
* would be re-centred on its own bounding box and land back in the same spot.
|
|
2007
|
+
*
|
|
2008
|
+
* `textAlign` is switched to centre for the pass, which is how fabric centres
|
|
2009
|
+
* a line on its path; the authored value is restored before anything else
|
|
2010
|
+
* reads it.
|
|
2011
|
+
*/
|
|
2012
|
+
patchLineMeasure(text, paths, union) {
|
|
2013
|
+
this.unpatchLineMeasure(text);
|
|
2014
|
+
const original = text._measureLine;
|
|
2015
|
+
if (typeof original !== "function") return;
|
|
2016
|
+
for (const path of paths) path.pathOffset = union.pathOffset;
|
|
2017
|
+
const installed = function(lineIndex) {
|
|
2018
|
+
const path = paths[lineIndex];
|
|
2019
|
+
if (!path) return original.call(this, lineIndex);
|
|
2020
|
+
const previousPath = this.path;
|
|
2021
|
+
const previousAlign = this.textAlign;
|
|
2022
|
+
this.path = path;
|
|
2023
|
+
this.textAlign = "center";
|
|
2024
|
+
this.setPathInfo?.();
|
|
2025
|
+
try {
|
|
2026
|
+
return original.call(this, lineIndex);
|
|
2027
|
+
} finally {
|
|
2028
|
+
this.path = previousPath;
|
|
2029
|
+
this.textAlign = previousAlign;
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "_measureLine");
|
|
2033
|
+
text._measureLine = installed;
|
|
2034
|
+
patches.set(text, { original, installed, ownedOriginal });
|
|
2035
|
+
}
|
|
2036
|
+
/** Restore fabric's own per-line measuring, if this manager replaced it. */
|
|
2037
|
+
unpatchLineMeasure(text) {
|
|
2038
|
+
const patch = patches.get(text);
|
|
2039
|
+
if (!patch) return;
|
|
2040
|
+
if (text._measureLine === patch.installed) {
|
|
2041
|
+
if (patch.ownedOriginal) text._measureLine = patch.original;
|
|
2042
|
+
else delete text._measureLine;
|
|
1621
2043
|
}
|
|
2044
|
+
patches.delete(text);
|
|
1622
2045
|
}
|
|
1623
2046
|
detach(text, authoredWidth) {
|
|
2047
|
+
this.unpatchLineMeasure(text);
|
|
1624
2048
|
text.set({ path: null, pathStartOffset: 0 });
|
|
1625
2049
|
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1626
2050
|
}
|
|
@@ -2151,6 +2575,11 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
2151
2575
|
if (serialized.opacity !== 1) {
|
|
2152
2576
|
editor.layers.setOpacity(layer.id, serialized.opacity);
|
|
2153
2577
|
}
|
|
2578
|
+
const childData = serialized.children ?? serialized.meta?.groupChildren;
|
|
2579
|
+
if (layer.type === "group" && Array.isArray(childData)) {
|
|
2580
|
+
editor.layers.adoptGroupChildren(layer, childData);
|
|
2581
|
+
if (layer.meta.groupChildren) delete layer.meta.groupChildren;
|
|
2582
|
+
}
|
|
2154
2583
|
return layer;
|
|
2155
2584
|
}
|
|
2156
2585
|
|
|
@@ -2726,10 +3155,10 @@ var MaskController = class {
|
|
|
2726
3155
|
};
|
|
2727
3156
|
|
|
2728
3157
|
// src/masks/manager.ts
|
|
2729
|
-
import { Group as
|
|
3158
|
+
import { Group as Group6 } from "fabric";
|
|
2730
3159
|
|
|
2731
3160
|
// src/masks/compose.ts
|
|
2732
|
-
import { Group, Rect as Rect2 } from "fabric";
|
|
3161
|
+
import { Group as Group2, Rect as Rect2 } from "fabric";
|
|
2733
3162
|
var MODE_OPERATION = {
|
|
2734
3163
|
add: "source-over",
|
|
2735
3164
|
subtract: "destination-out",
|
|
@@ -2766,7 +3195,7 @@ function composeMaskGroup(children, entries, options) {
|
|
|
2766
3195
|
});
|
|
2767
3196
|
const first = entries.find((entry) => entry.visible);
|
|
2768
3197
|
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
2769
|
-
return new
|
|
3198
|
+
return new Group2(withBase, {
|
|
2770
3199
|
absolutePositioned: options.absolute,
|
|
2771
3200
|
// Cached, so the children's compositing operations resolve against each
|
|
2772
3201
|
// other instead of against the page underneath the mask.
|
|
@@ -2942,7 +3371,7 @@ var MaskEditController = class {
|
|
|
2942
3371
|
};
|
|
2943
3372
|
|
|
2944
3373
|
// src/masks/store.ts
|
|
2945
|
-
import { Group as
|
|
3374
|
+
import { Group as Group5 } from "fabric";
|
|
2946
3375
|
|
|
2947
3376
|
// src/masks/host.ts
|
|
2948
3377
|
import { Rect as Rect3 } from "fabric";
|
|
@@ -2999,9 +3428,9 @@ function hostBoxOf(canvas, host, absolute) {
|
|
|
2999
3428
|
}
|
|
3000
3429
|
|
|
3001
3430
|
// src/masks/install.ts
|
|
3002
|
-
import { Group as
|
|
3431
|
+
import { Group as Group4 } from "fabric";
|
|
3003
3432
|
function convertSpace(host, sources, absolute) {
|
|
3004
|
-
const wasAbsolute = host.clipPath instanceof
|
|
3433
|
+
const wasAbsolute = host.clipPath instanceof Group4 ? host.clipPath.absolutePositioned : absolute;
|
|
3005
3434
|
if (absolute === wasAbsolute) return;
|
|
3006
3435
|
for (const source of sources) {
|
|
3007
3436
|
if (absolute) toCanvasSpace(source, host);
|
|
@@ -3093,7 +3522,7 @@ var MaskStackStore = class {
|
|
|
3093
3522
|
const clip = host.clipPath;
|
|
3094
3523
|
if (!clip) return [];
|
|
3095
3524
|
const entries = this.list(target);
|
|
3096
|
-
if (entries.length === 0 || !(clip instanceof
|
|
3525
|
+
if (entries.length === 0 || !(clip instanceof Group5)) return [asObject(clip)];
|
|
3097
3526
|
const children = unwrapGroup(clip);
|
|
3098
3527
|
const extra = children.length - entries.length;
|
|
3099
3528
|
return extra > 0 ? children.slice(extra) : children;
|
|
@@ -3193,7 +3622,7 @@ var LayerMaskManager = class extends MaskStackStore {
|
|
|
3193
3622
|
if (index === -1) return false;
|
|
3194
3623
|
this.endEdit(false);
|
|
3195
3624
|
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3196
|
-
const group = host.clipPath instanceof
|
|
3625
|
+
const group = host.clipPath instanceof Group6 ? host.clipPath : null;
|
|
3197
3626
|
if (!group) return false;
|
|
3198
3627
|
const children = group.getObjects();
|
|
3199
3628
|
const child = children[children.length - entries.length + index];
|
|
@@ -3588,7 +4017,7 @@ var CanvasEditor = class {
|
|
|
3588
4017
|
}
|
|
3589
4018
|
}
|
|
3590
4019
|
addText(text, options) {
|
|
3591
|
-
const textbox = new
|
|
4020
|
+
const textbox = new Textbox2(text, {
|
|
3592
4021
|
fontSize: 32,
|
|
3593
4022
|
fontFamily: "Arial",
|
|
3594
4023
|
fill: "#000000",
|
|
@@ -3629,20 +4058,30 @@ var CanvasEditor = class {
|
|
|
3629
4058
|
return value === void 0 ? token : escapeXml(value);
|
|
3630
4059
|
})
|
|
3631
4060
|
);
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
4061
|
+
const specs = resolved.includes("<textPath") ? [textPathSpec(template, values)] : await explodeTemplateSvg(template, resolved, values);
|
|
4062
|
+
if (specs.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
4063
|
+
return this.history.transaction(async () => {
|
|
4064
|
+
const created = specs.map((spec) => {
|
|
4065
|
+
const layer2 = this.layers.add(spec.type, spec.object, spec.name);
|
|
4066
|
+
if (spec.curve) this.curves.apply(layer2.id, spec.curve, false);
|
|
4067
|
+
return layer2;
|
|
4068
|
+
});
|
|
4069
|
+
const layer = created.length === 1 ? created[0] : await this.groupLayers(
|
|
4070
|
+
created.map((l) => l.id),
|
|
4071
|
+
template.name
|
|
4072
|
+
) ?? created[0];
|
|
4073
|
+
layer.fabricObject.set({
|
|
4074
|
+
left: this.canvas.getWidth() / 2,
|
|
4075
|
+
top: this.canvas.getHeight() / 2,
|
|
4076
|
+
originX: "center",
|
|
4077
|
+
originY: "center"
|
|
4078
|
+
});
|
|
4079
|
+
layer.fabricObject.setCoords();
|
|
4080
|
+
this.layers.select(layer.id);
|
|
4081
|
+
this.canvas.requestRenderAll();
|
|
4082
|
+
this.history.save();
|
|
4083
|
+
return layer;
|
|
3641
4084
|
});
|
|
3642
|
-
const layer = this.layers.add("template", group, template.name);
|
|
3643
|
-
this.layers.select(layer.id);
|
|
3644
|
-
this.history.save();
|
|
3645
|
-
return layer;
|
|
3646
4085
|
}
|
|
3647
4086
|
removeLayer(id) {
|
|
3648
4087
|
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
@@ -3654,6 +4093,10 @@ var CanvasEditor = class {
|
|
|
3654
4093
|
selectLayer(id) {
|
|
3655
4094
|
this.layers.select(id);
|
|
3656
4095
|
}
|
|
4096
|
+
/** Select several layers at once — what a panel's shift/⌘ click drives. */
|
|
4097
|
+
selectLayers(ids) {
|
|
4098
|
+
this.layers.selectMany(ids);
|
|
4099
|
+
}
|
|
3657
4100
|
getSelectedLayer() {
|
|
3658
4101
|
const active = this.canvas.getActiveObject();
|
|
3659
4102
|
if (!active) return null;
|
|
@@ -3716,42 +4159,55 @@ var CanvasEditor = class {
|
|
|
3716
4159
|
this.history.save();
|
|
3717
4160
|
return true;
|
|
3718
4161
|
}
|
|
3719
|
-
/**
|
|
4162
|
+
/**
|
|
4163
|
+
* Combine two or more layers into one group layer. Picking a group among them
|
|
4164
|
+
* **absorbs** it — its children join the new group and the old group record is
|
|
4165
|
+
* dropped — because groups are exactly one level deep.
|
|
4166
|
+
*/
|
|
3720
4167
|
async groupLayers(ids, name = "Group") {
|
|
3721
4168
|
const uniqueIds = [...new Set(ids)];
|
|
3722
|
-
const
|
|
3723
|
-
if (
|
|
4169
|
+
const picked = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0 && layer.parentId === null);
|
|
4170
|
+
if (picked.length < 2 || picked.length !== uniqueIds.length) return null;
|
|
4171
|
+
const stack = this.layers.getAll();
|
|
4172
|
+
const topIndex = Math.max(...picked.map((layer) => stack.indexOf(layer)));
|
|
4173
|
+
const insertIndex = stack.slice(0, topIndex).filter((layer) => !picked.includes(layer)).length;
|
|
3724
4174
|
return this.history.transaction(() => {
|
|
3725
|
-
const
|
|
3726
|
-
const objects =
|
|
3727
|
-
for (const layer of
|
|
3728
|
-
|
|
4175
|
+
const children = [];
|
|
4176
|
+
const objects = [];
|
|
4177
|
+
for (const layer of picked) {
|
|
4178
|
+
if (layer.type === "group" && layer.children.length > 0) {
|
|
4179
|
+
const inner = layer.fabricObject.removeAll();
|
|
4180
|
+
for (const [index, object] of inner.entries()) {
|
|
4181
|
+
object.setCoords();
|
|
4182
|
+
const child = layer.children[index];
|
|
4183
|
+
if (!child) continue;
|
|
4184
|
+
children.push(child);
|
|
4185
|
+
objects.push(object);
|
|
4186
|
+
}
|
|
4187
|
+
layer.children = [];
|
|
4188
|
+
this.layers.detach(layer.id);
|
|
4189
|
+
} else {
|
|
4190
|
+
children.push(layer);
|
|
4191
|
+
objects.push(layer.fabricObject);
|
|
4192
|
+
this.layers.detach(layer.id);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
const group = new Group7(objects);
|
|
3729
4196
|
const grouped = this.layers.add("group", group, name);
|
|
3730
|
-
|
|
4197
|
+
for (const child of children) child.parentId = grouped.id;
|
|
4198
|
+
grouped.children = children;
|
|
4199
|
+
this.layers.reorder(grouped.id, insertIndex);
|
|
3731
4200
|
this.layers.select(grouped.id);
|
|
3732
4201
|
this.history.save();
|
|
3733
4202
|
return grouped;
|
|
3734
4203
|
});
|
|
3735
4204
|
}
|
|
3736
|
-
/**
|
|
4205
|
+
/** Split a group back into the layers it holds, in place. */
|
|
3737
4206
|
async ungroupLayer(id) {
|
|
3738
4207
|
const grouped = this.layers.get(id);
|
|
3739
|
-
|
|
3740
|
-
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
3741
|
-
const group = grouped.fabricObject;
|
|
4208
|
+
if (!grouped || grouped.type !== "group" || grouped.children.length === 0) return [];
|
|
3742
4209
|
return this.history.transaction(() => {
|
|
3743
|
-
const
|
|
3744
|
-
this.layers.remove(id);
|
|
3745
|
-
const restored = objects.map((object, index) => {
|
|
3746
|
-
object.setCoords();
|
|
3747
|
-
const data = childData[index];
|
|
3748
|
-
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
3749
|
-
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
3750
|
-
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
3751
|
-
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
3752
|
-
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
3753
|
-
return layer;
|
|
3754
|
-
});
|
|
4210
|
+
const restored = this.layers.explode(grouped);
|
|
3755
4211
|
this.history.save();
|
|
3756
4212
|
this.layers.select(restored[0]?.id ?? null);
|
|
3757
4213
|
return restored;
|
|
@@ -4498,6 +4954,7 @@ export {
|
|
|
4498
4954
|
applyLayerShadow,
|
|
4499
4955
|
applyObjectSelectionStyle,
|
|
4500
4956
|
applySelectionStyle,
|
|
4957
|
+
buildCurveLinePaths,
|
|
4501
4958
|
buildCurvePathData,
|
|
4502
4959
|
clamp,
|
|
4503
4960
|
clearTextureMaskCache,
|
|
@@ -4509,6 +4966,7 @@ export {
|
|
|
4509
4966
|
displaceRgba,
|
|
4510
4967
|
drawTiles,
|
|
4511
4968
|
escapeXml,
|
|
4969
|
+
explodeTemplateSvg,
|
|
4512
4970
|
exportDataURL,
|
|
4513
4971
|
exportMockup,
|
|
4514
4972
|
exportPNG,
|
|
@@ -4521,6 +4979,7 @@ export {
|
|
|
4521
4979
|
isShapeMaskId,
|
|
4522
4980
|
isTextureMaskId,
|
|
4523
4981
|
needsAbsoluteSpace,
|
|
4982
|
+
normalizeTextCurve,
|
|
4524
4983
|
readLayerShadow,
|
|
4525
4984
|
renderTextureMask,
|
|
4526
4985
|
resetTransform,
|
|
@@ -4529,6 +4988,8 @@ export {
|
|
|
4529
4988
|
sanitizeSvg,
|
|
4530
4989
|
serializeEditor,
|
|
4531
4990
|
shapeMaskPathData,
|
|
4991
|
+
textPathCurve,
|
|
4992
|
+
textPathSpec,
|
|
4532
4993
|
toCanvasSpace,
|
|
4533
4994
|
toHostSpace,
|
|
4534
4995
|
unwrapGroup
|