@overtone-art/canvas-editor-core 0.5.1 → 0.6.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.d.mts +139 -14
- package/dist/index.d.ts +139 -14
- package/dist/index.global.js +52 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +620 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +571 -120
- 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.js
CHANGED
|
@@ -53,6 +53,7 @@ __export(index_exports, {
|
|
|
53
53
|
applyLayerShadow: () => applyLayerShadow,
|
|
54
54
|
applyObjectSelectionStyle: () => applyObjectSelectionStyle,
|
|
55
55
|
applySelectionStyle: () => applySelectionStyle,
|
|
56
|
+
buildCurveLinePaths: () => buildCurveLinePaths,
|
|
56
57
|
buildCurvePathData: () => buildCurvePathData,
|
|
57
58
|
clamp: () => clamp,
|
|
58
59
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
@@ -64,6 +65,7 @@ __export(index_exports, {
|
|
|
64
65
|
displaceRgba: () => displaceRgba,
|
|
65
66
|
drawTiles: () => drawTiles,
|
|
66
67
|
escapeXml: () => escapeXml,
|
|
68
|
+
explodeTemplateSvg: () => explodeTemplateSvg,
|
|
67
69
|
exportDataURL: () => exportDataURL,
|
|
68
70
|
exportMockup: () => exportMockup,
|
|
69
71
|
exportPNG: () => exportPNG,
|
|
@@ -76,6 +78,7 @@ __export(index_exports, {
|
|
|
76
78
|
isShapeMaskId: () => isShapeMaskId,
|
|
77
79
|
isTextureMaskId: () => isTextureMaskId,
|
|
78
80
|
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
81
|
+
normalizeTextCurve: () => normalizeTextCurve,
|
|
79
82
|
readLayerShadow: () => readLayerShadow,
|
|
80
83
|
renderTextureMask: () => renderTextureMask,
|
|
81
84
|
resetTransform: () => resetTransform,
|
|
@@ -84,6 +87,8 @@ __export(index_exports, {
|
|
|
84
87
|
sanitizeSvg: () => sanitizeSvg,
|
|
85
88
|
serializeEditor: () => serializeEditor,
|
|
86
89
|
shapeMaskPathData: () => shapeMaskPathData,
|
|
90
|
+
textPathCurve: () => textPathCurve,
|
|
91
|
+
textPathSpec: () => textPathSpec,
|
|
87
92
|
toCanvasSpace: () => toCanvasSpace,
|
|
88
93
|
toHostSpace: () => toHostSpace,
|
|
89
94
|
unwrapGroup: () => unwrapGroup
|
|
@@ -91,7 +96,7 @@ __export(index_exports, {
|
|
|
91
96
|
module.exports = __toCommonJS(index_exports);
|
|
92
97
|
|
|
93
98
|
// src/editor.ts
|
|
94
|
-
var
|
|
99
|
+
var import_fabric19 = require("fabric");
|
|
95
100
|
|
|
96
101
|
// src/events.ts
|
|
97
102
|
var EventEmitter = class {
|
|
@@ -128,6 +133,9 @@ var EventEmitter = class {
|
|
|
128
133
|
}
|
|
129
134
|
};
|
|
130
135
|
|
|
136
|
+
// src/layer.ts
|
|
137
|
+
var import_fabric = require("fabric");
|
|
138
|
+
|
|
131
139
|
// src/utils/id.ts
|
|
132
140
|
var import_nanoid = require("nanoid");
|
|
133
141
|
var generateId = () => (0, import_nanoid.nanoid)(12);
|
|
@@ -149,6 +157,14 @@ var Layer = class {
|
|
|
149
157
|
* the layer's real object stays the one every editor API talks to.
|
|
150
158
|
*/
|
|
151
159
|
renderProxy;
|
|
160
|
+
/**
|
|
161
|
+
* Layers nested inside this one, in draw order. Only a `group` layer has any:
|
|
162
|
+
* their fabric objects live inside this layer's fabric `Group`, not on the
|
|
163
|
+
* canvas, which is why the manager keeps them off the canvas stack.
|
|
164
|
+
*/
|
|
165
|
+
children;
|
|
166
|
+
/** Group this layer sits inside, or null when it is on the canvas stack. */
|
|
167
|
+
parentId;
|
|
152
168
|
constructor(type, fabricObject, name, id) {
|
|
153
169
|
this.id = id ?? generateId();
|
|
154
170
|
this.type = type;
|
|
@@ -159,6 +175,8 @@ var Layer = class {
|
|
|
159
175
|
this.meta = {};
|
|
160
176
|
this.fabricObject = fabricObject;
|
|
161
177
|
this.renderProxy = null;
|
|
178
|
+
this.children = [];
|
|
179
|
+
this.parentId = null;
|
|
162
180
|
this.fabricObject._layerId = this.id;
|
|
163
181
|
}
|
|
164
182
|
hasMeta() {
|
|
@@ -172,12 +190,15 @@ var Layer = class {
|
|
|
172
190
|
visible: this.visible,
|
|
173
191
|
locked: this.locked,
|
|
174
192
|
opacity: this.opacity,
|
|
175
|
-
...this.hasMeta() ? { meta: this.meta } : {}
|
|
193
|
+
...this.hasMeta() ? { meta: this.meta } : {},
|
|
194
|
+
...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
|
|
176
195
|
};
|
|
177
196
|
}
|
|
178
197
|
serialize() {
|
|
179
198
|
return {
|
|
180
199
|
...this.toData(),
|
|
200
|
+
// A group's own object already carries its children's geometry, so the
|
|
201
|
+
// child records in `toData` stay identity-only — nothing is duplicated.
|
|
181
202
|
fabricObject: this.fabricObject.toObject()
|
|
182
203
|
};
|
|
183
204
|
}
|
|
@@ -203,9 +224,23 @@ var LayerManager = class {
|
|
|
203
224
|
return layer;
|
|
204
225
|
}
|
|
205
226
|
remove(id) {
|
|
227
|
+
const layer = this.get(id);
|
|
228
|
+
if (!layer) return false;
|
|
229
|
+
if (layer.parentId) {
|
|
230
|
+
const parent = this.get(layer.parentId);
|
|
231
|
+
if (!parent) return false;
|
|
232
|
+
parent.fabricObject.remove(layer.fabricObject);
|
|
233
|
+
parent.children = parent.children.filter((c) => c.id !== id);
|
|
234
|
+
layer.parentId = null;
|
|
235
|
+
this.events.emit("layer:removed", { layerId: id });
|
|
236
|
+
if (parent.children.length < 2) this.dissolve(parent);
|
|
237
|
+
this.emitChanged();
|
|
238
|
+
this.onPropertyChanged?.();
|
|
239
|
+
this.canvas.requestRenderAll();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
206
242
|
const index = this.layers.findIndex((l) => l.id === id);
|
|
207
243
|
if (index === -1) return false;
|
|
208
|
-
const layer = this.layers[index];
|
|
209
244
|
this.canvas.remove(layer.fabricObject);
|
|
210
245
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
211
246
|
this.layers.splice(index, 1);
|
|
@@ -214,6 +249,81 @@ var LayerManager = class {
|
|
|
214
249
|
this.onPropertyChanged?.();
|
|
215
250
|
return true;
|
|
216
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Take a top-level layer off the canvas stack **without** disposing it — it is
|
|
254
|
+
* about to become a group's child, so its object has to stay alive.
|
|
255
|
+
*/
|
|
256
|
+
detach(id) {
|
|
257
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
258
|
+
if (index === -1) return void 0;
|
|
259
|
+
const [layer] = this.layers.splice(index, 1);
|
|
260
|
+
this.canvas.remove(layer.fabricObject);
|
|
261
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
262
|
+
return layer;
|
|
263
|
+
}
|
|
264
|
+
/** Put a group's children back on the canvas stack and drop the group. */
|
|
265
|
+
explode(group) {
|
|
266
|
+
const children = [...group.children];
|
|
267
|
+
this.dissolve(group);
|
|
268
|
+
this.events.emit("layer:removed", { layerId: group.id });
|
|
269
|
+
this.emitChanged();
|
|
270
|
+
this.onPropertyChanged?.();
|
|
271
|
+
this.canvas.requestRenderAll();
|
|
272
|
+
return children;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Empty a group onto the canvas stack, in the group's own place, and drop the
|
|
276
|
+
* group record. `removeAll` restores each child's absolute transform, so the
|
|
277
|
+
* group's matrix must NOT be re-applied on top of it.
|
|
278
|
+
*/
|
|
279
|
+
dissolve(group) {
|
|
280
|
+
const objects = group.fabricObject.removeAll();
|
|
281
|
+
const at = this.layers.findIndex((l) => l.id === group.id);
|
|
282
|
+
const survivors = group.children;
|
|
283
|
+
group.children = [];
|
|
284
|
+
for (const [index, object] of objects.entries()) {
|
|
285
|
+
object.setCoords();
|
|
286
|
+
const child = survivors[index];
|
|
287
|
+
if (!child) continue;
|
|
288
|
+
child.parentId = null;
|
|
289
|
+
this.layers.splice(at + index, 0, child);
|
|
290
|
+
this.canvas.add(object);
|
|
291
|
+
if (child.renderProxy) this.canvas.add(child.renderProxy);
|
|
292
|
+
}
|
|
293
|
+
this.canvas.remove(group.fabricObject);
|
|
294
|
+
this.layers = this.layers.filter((l) => l.id !== group.id);
|
|
295
|
+
this.syncZOrder();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Re-attach child records to an enlivened group. `data[i]` describes
|
|
299
|
+
* `group._objects[i]` — the order fabric serializes and restores them in. A
|
|
300
|
+
* missing record still becomes a child, so a hand-edited state cannot drop
|
|
301
|
+
* artwork off the canvas.
|
|
302
|
+
*/
|
|
303
|
+
adoptGroupChildren(group, data) {
|
|
304
|
+
const objects = group.fabricObject.getObjects();
|
|
305
|
+
group.children = objects.map((object, index) => {
|
|
306
|
+
const record = data[index];
|
|
307
|
+
const child = new Layer(record?.type ?? "shape", object, record?.name, record?.id);
|
|
308
|
+
child.parentId = group.id;
|
|
309
|
+
if (record?.meta) child.meta = structuredClone(record.meta);
|
|
310
|
+
if (record && !record.visible) {
|
|
311
|
+
child.visible = false;
|
|
312
|
+
object.visible = false;
|
|
313
|
+
}
|
|
314
|
+
if (record?.locked) {
|
|
315
|
+
child.locked = true;
|
|
316
|
+
object.selectable = false;
|
|
317
|
+
object.evented = false;
|
|
318
|
+
}
|
|
319
|
+
if (record && record.opacity !== 1) {
|
|
320
|
+
child.opacity = record.opacity;
|
|
321
|
+
object.opacity = record.opacity;
|
|
322
|
+
}
|
|
323
|
+
return child;
|
|
324
|
+
});
|
|
325
|
+
this.emitChanged();
|
|
326
|
+
}
|
|
217
327
|
/** Replace a layer's render object while preserving its immutable ID and panel state. */
|
|
218
328
|
replaceObject(id, fabricObject) {
|
|
219
329
|
const layer = this.get(id);
|
|
@@ -278,16 +388,32 @@ var LayerManager = class {
|
|
|
278
388
|
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
279
389
|
}
|
|
280
390
|
}
|
|
391
|
+
/** The list a layer lives in: its group's children, or the canvas stack. */
|
|
392
|
+
siblingsOf(layer) {
|
|
393
|
+
if (!layer.parentId) return this.layers;
|
|
394
|
+
return this.get(layer.parentId)?.children ?? this.layers;
|
|
395
|
+
}
|
|
281
396
|
reorder(id, newIndex) {
|
|
282
|
-
const
|
|
397
|
+
const target = this.get(id);
|
|
398
|
+
if (!target) return false;
|
|
399
|
+
const list = this.siblingsOf(target);
|
|
400
|
+
const oldIndex = list.indexOf(target);
|
|
283
401
|
if (oldIndex === -1) return false;
|
|
284
402
|
if (!Number.isFinite(newIndex)) return false;
|
|
285
|
-
const clamped = Math.max(0, Math.min(
|
|
403
|
+
const clamped = Math.max(0, Math.min(list.length - 1, Math.round(newIndex)));
|
|
286
404
|
if (oldIndex === clamped) return false;
|
|
287
|
-
const [layer] =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
405
|
+
const [layer] = list.splice(oldIndex, 1);
|
|
406
|
+
list.splice(clamped, 0, layer);
|
|
407
|
+
if (layer.parentId) {
|
|
408
|
+
const group = this.get(layer.parentId)?.fabricObject;
|
|
409
|
+
if (group) {
|
|
410
|
+
group.remove(layer.fabricObject);
|
|
411
|
+
group.insertAt(clamped, layer.fabricObject);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
this.syncZOrder();
|
|
415
|
+
}
|
|
416
|
+
this.events.emit("layer:reordered", { layerIds: list.map((l) => l.id) });
|
|
291
417
|
this.emitChanged();
|
|
292
418
|
this.onPropertyChanged?.();
|
|
293
419
|
return true;
|
|
@@ -295,9 +421,12 @@ var LayerManager = class {
|
|
|
295
421
|
select(id) {
|
|
296
422
|
if (id === null) {
|
|
297
423
|
this.canvas.discardActiveObject();
|
|
424
|
+
this.exitGroups();
|
|
298
425
|
} else {
|
|
299
426
|
const layer = this.get(id);
|
|
300
427
|
if (layer) {
|
|
428
|
+
if (layer.parentId) this.enterGroup(layer.parentId);
|
|
429
|
+
else this.exitGroups();
|
|
301
430
|
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
302
431
|
}
|
|
303
432
|
}
|
|
@@ -306,16 +435,67 @@ var LayerManager = class {
|
|
|
306
435
|
const selected = id && this.get(id) ? [id] : [];
|
|
307
436
|
this.events.emit("selection:changed", { selected });
|
|
308
437
|
}
|
|
438
|
+
/** Drill into a group so its children can be selected and clicked. */
|
|
439
|
+
enterGroup(groupId) {
|
|
440
|
+
const parent = this.get(groupId);
|
|
441
|
+
if (!parent) return;
|
|
442
|
+
const group = parent.fabricObject;
|
|
443
|
+
group.subTargetCheck = true;
|
|
444
|
+
group.interactive = true;
|
|
445
|
+
}
|
|
446
|
+
/** Leave every entered group — selecting anything else is done drilling in. */
|
|
447
|
+
exitGroups() {
|
|
448
|
+
for (const layer of this.layers) {
|
|
449
|
+
if (layer.type !== "group") continue;
|
|
450
|
+
const group = layer.fabricObject;
|
|
451
|
+
if (group.interactive) group.interactive = false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Select several layers at once, as one fabric `ActiveSelection`. Children are
|
|
456
|
+
* skipped: a selection spanning in and out of a group has no single transform.
|
|
457
|
+
*/
|
|
458
|
+
selectMany(ids) {
|
|
459
|
+
const layers = ids.flatMap((id) => {
|
|
460
|
+
const layer = this.get(id);
|
|
461
|
+
return layer && !layer.parentId ? [layer] : [];
|
|
462
|
+
});
|
|
463
|
+
if (layers.length === 0) return this.select(null);
|
|
464
|
+
if (layers.length === 1) return this.select(layers[0].id);
|
|
465
|
+
this.canvas.discardActiveObject();
|
|
466
|
+
this.exitGroups();
|
|
467
|
+
const selection = new import_fabric.ActiveSelection(
|
|
468
|
+
layers.map((l) => l.renderProxy ?? l.fabricObject),
|
|
469
|
+
{ canvas: this.canvas }
|
|
470
|
+
);
|
|
471
|
+
this.canvas.setActiveObject(selection);
|
|
472
|
+
this.canvas.requestRenderAll();
|
|
473
|
+
this.events.emit("selection:changed", { selected: layers.map((l) => l.id) });
|
|
474
|
+
}
|
|
475
|
+
/** Depth-first over the whole tree, each layer before its children. */
|
|
476
|
+
*walk(layers = this.layers) {
|
|
477
|
+
for (const layer of layers) {
|
|
478
|
+
yield layer;
|
|
479
|
+
if (layer.children.length > 0) yield* this.walk(layer.children);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
309
482
|
get(id) {
|
|
310
|
-
|
|
483
|
+
for (const layer of this.walk()) if (layer.id === id) return layer;
|
|
484
|
+
return void 0;
|
|
311
485
|
}
|
|
486
|
+
/** Top-level layers only — the canvas stack. Children hang off their group. */
|
|
312
487
|
getAll() {
|
|
313
488
|
return [...this.layers];
|
|
314
489
|
}
|
|
490
|
+
/** Top-level layers, each carrying its children: what the layer panel renders. */
|
|
491
|
+
getTree() {
|
|
492
|
+
return this.layers.map((l) => l.toData());
|
|
493
|
+
}
|
|
315
494
|
findByObject(obj) {
|
|
316
495
|
const layerId = obj._layerId;
|
|
317
496
|
if (layerId) return this.get(layerId);
|
|
318
|
-
|
|
497
|
+
for (const layer of this.walk()) if (layer.fabricObject === obj) return layer;
|
|
498
|
+
return void 0;
|
|
319
499
|
}
|
|
320
500
|
setVisibility(id, visible) {
|
|
321
501
|
const layer = this.get(id);
|
|
@@ -396,6 +576,7 @@ var LayerManager = class {
|
|
|
396
576
|
for (const layer of this.layers) {
|
|
397
577
|
this.canvas.remove(layer.fabricObject);
|
|
398
578
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
579
|
+
layer.children = [];
|
|
399
580
|
}
|
|
400
581
|
this.layers = [];
|
|
401
582
|
this.emitChanged();
|
|
@@ -410,6 +591,107 @@ var LayerManager = class {
|
|
|
410
591
|
}
|
|
411
592
|
};
|
|
412
593
|
|
|
594
|
+
// src/template-explode.ts
|
|
595
|
+
var import_fabric2 = require("fabric");
|
|
596
|
+
var NAME_MAX = 24;
|
|
597
|
+
function isText(object) {
|
|
598
|
+
return typeof object.text === "string";
|
|
599
|
+
}
|
|
600
|
+
function labelsByRun(template, values) {
|
|
601
|
+
const labels = new Map(template.parameters.map((p) => [p.key, p.label || p.key]));
|
|
602
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const match of template.svg.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g)) {
|
|
604
|
+
const token = /\{\{(\w+)(?:[|:]([^}]*))?\}\}/.exec(match[1]);
|
|
605
|
+
if (!token) continue;
|
|
606
|
+
const label = labels.get(token[1]);
|
|
607
|
+
if (!label) continue;
|
|
608
|
+
const resolved = String(values[token[1]] ?? token[2] ?? "").trim();
|
|
609
|
+
if (resolved) byRun.set(resolved, label);
|
|
610
|
+
}
|
|
611
|
+
return byRun;
|
|
612
|
+
}
|
|
613
|
+
function textName(run, labels) {
|
|
614
|
+
const trimmed = run.trim();
|
|
615
|
+
const label = labels.get(trimmed);
|
|
616
|
+
if (label) return label;
|
|
617
|
+
if (!trimmed) return "Text";
|
|
618
|
+
return trimmed.length > NAME_MAX ? `${trimmed.slice(0, NAME_MAX - 1)}\u2026` : trimmed;
|
|
619
|
+
}
|
|
620
|
+
function shapeName(object) {
|
|
621
|
+
const id = object.id;
|
|
622
|
+
if (typeof id === "string" && id) return id;
|
|
623
|
+
const type = object.type || "shape";
|
|
624
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
625
|
+
}
|
|
626
|
+
function toTextbox(text) {
|
|
627
|
+
const box = new import_fabric2.Textbox(text.text, {
|
|
628
|
+
left: text.left,
|
|
629
|
+
top: text.top,
|
|
630
|
+
originX: text.originX,
|
|
631
|
+
originY: text.originY,
|
|
632
|
+
angle: text.angle,
|
|
633
|
+
scaleX: text.scaleX,
|
|
634
|
+
scaleY: text.scaleY,
|
|
635
|
+
fontSize: text.fontSize,
|
|
636
|
+
fontFamily: text.fontFamily,
|
|
637
|
+
fontWeight: text.fontWeight,
|
|
638
|
+
fontStyle: text.fontStyle,
|
|
639
|
+
textAlign: text.textAlign,
|
|
640
|
+
charSpacing: text.charSpacing,
|
|
641
|
+
lineHeight: text.lineHeight,
|
|
642
|
+
fill: text.fill,
|
|
643
|
+
stroke: text.stroke,
|
|
644
|
+
strokeWidth: text.strokeWidth,
|
|
645
|
+
opacity: text.opacity,
|
|
646
|
+
// A Textbox wraps at its box width; size it to the run the template drew so
|
|
647
|
+
// the authored copy never comes in pre-wrapped.
|
|
648
|
+
width: Math.max(1, (text.width ?? 0) + 2)
|
|
649
|
+
});
|
|
650
|
+
return box;
|
|
651
|
+
}
|
|
652
|
+
function textPathCurve(template, values, runWidth) {
|
|
653
|
+
if (template.id === "circular-text") return { arc: 100, wave: 0 };
|
|
654
|
+
if (template.id === "wave-text") return { arc: 0, wave: 50 };
|
|
655
|
+
const radius = Number(values.radius ?? 200);
|
|
656
|
+
if (!Number.isFinite(radius) || radius <= 0) return { arc: 0, wave: 0 };
|
|
657
|
+
const arc = 100 * runWidth / (Math.PI * radius);
|
|
658
|
+
return { arc: Math.max(-100, Math.min(100, arc)), wave: 0 };
|
|
659
|
+
}
|
|
660
|
+
function textPathSpec(template, values) {
|
|
661
|
+
const content = String(values.text ?? "Text");
|
|
662
|
+
const fontSize = template.id === "circular-text" ? 28 : 40;
|
|
663
|
+
const box = new import_fabric2.Textbox(content, {
|
|
664
|
+
fontSize,
|
|
665
|
+
fontFamily: String(values.font ?? "Arial"),
|
|
666
|
+
fontWeight: "bold",
|
|
667
|
+
fill: String(values.color ?? "#000000"),
|
|
668
|
+
originX: "left",
|
|
669
|
+
originY: "top",
|
|
670
|
+
// Wide enough that the run stays on one line before the curve widens it.
|
|
671
|
+
width: Math.max(1, content.length * fontSize * 0.62)
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
type: "text",
|
|
675
|
+
name: template.parameters.find((p) => p.key === "text")?.label || "Text",
|
|
676
|
+
object: box,
|
|
677
|
+
curve: textPathCurve(template, values, box.width ?? 0)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function explodeTemplateSvg(template, resolvedSvg, values) {
|
|
681
|
+
const labels = labelsByRun(template, values);
|
|
682
|
+
const { objects } = await (0, import_fabric2.loadSVGFromString)(resolvedSvg);
|
|
683
|
+
const specs = [];
|
|
684
|
+
for (const object of objects) {
|
|
685
|
+
if (object === null) continue;
|
|
686
|
+
if (isText(object)) {
|
|
687
|
+
specs.push({ type: "text", name: textName(object.text, labels), object: toTextbox(object) });
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
specs.push({ type: "shape", name: shapeName(object), object });
|
|
691
|
+
}
|
|
692
|
+
return specs;
|
|
693
|
+
}
|
|
694
|
+
|
|
413
695
|
// src/history.ts
|
|
414
696
|
var HistoryManager = class _HistoryManager {
|
|
415
697
|
static ASSET_KEY = "__canvasEditorHistoryAsset";
|
|
@@ -797,7 +1079,7 @@ function bestSnap(edges, candidates, threshold) {
|
|
|
797
1079
|
}
|
|
798
1080
|
|
|
799
1081
|
// src/crop.ts
|
|
800
|
-
var
|
|
1082
|
+
var import_fabric3 = require("fabric");
|
|
801
1083
|
|
|
802
1084
|
// src/utils/clamp.ts
|
|
803
1085
|
function clamp(v, min, max) {
|
|
@@ -837,7 +1119,7 @@ var CropController = class {
|
|
|
837
1119
|
if (prevAngle) image.rotate(0);
|
|
838
1120
|
image.setCoords();
|
|
839
1121
|
const b = image.getBoundingRect();
|
|
840
|
-
const rect = new
|
|
1122
|
+
const rect = new import_fabric3.Rect({
|
|
841
1123
|
left: b.left,
|
|
842
1124
|
top: b.top,
|
|
843
1125
|
width: b.width,
|
|
@@ -930,10 +1212,10 @@ var CropController = class {
|
|
|
930
1212
|
var STROKE2 = "#22c55e";
|
|
931
1213
|
|
|
932
1214
|
// src/pattern/pattern-manager.ts
|
|
933
|
-
var
|
|
1215
|
+
var import_fabric5 = require("fabric");
|
|
934
1216
|
|
|
935
1217
|
// src/pattern/tiled-pattern-object.ts
|
|
936
|
-
var
|
|
1218
|
+
var import_fabric4 = require("fabric");
|
|
937
1219
|
|
|
938
1220
|
// src/pattern/tile-geometry.ts
|
|
939
1221
|
var MAX_TILES_PER_AXIS = 200;
|
|
@@ -1000,7 +1282,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
|
1000
1282
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
1001
1283
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1002
1284
|
var TILE_BLEED_DEVICE_PX = 2;
|
|
1003
|
-
var TiledPatternObject = class _TiledPatternObject extends
|
|
1285
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
|
|
1004
1286
|
static type = "TiledPattern";
|
|
1005
1287
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
1006
1288
|
source;
|
|
@@ -1083,7 +1365,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1083
1365
|
const { tileW, tileH } = this.liveTile();
|
|
1084
1366
|
const centre = this.getCenterPoint();
|
|
1085
1367
|
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1086
|
-
return new
|
|
1368
|
+
return new import_fabric4.Point(centre.x - shift.x, centre.y - shift.y);
|
|
1087
1369
|
}
|
|
1088
1370
|
_render(ctx) {
|
|
1089
1371
|
const { width, height } = this.area;
|
|
@@ -1260,11 +1542,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1260
1542
|
const radians = angle * Math.PI / 180;
|
|
1261
1543
|
const cos = Math.cos(radians);
|
|
1262
1544
|
const sin = Math.sin(radians);
|
|
1263
|
-
return new
|
|
1545
|
+
return new import_fabric4.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1264
1546
|
}
|
|
1265
1547
|
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1266
1548
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1267
|
-
return new
|
|
1549
|
+
return new import_fabric4.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1268
1550
|
}
|
|
1269
1551
|
/**
|
|
1270
1552
|
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
@@ -1524,7 +1806,7 @@ async function unbakeLegacyLayer(layer, state) {
|
|
|
1524
1806
|
cropY: state.original.cropY,
|
|
1525
1807
|
angle: state.original.angle
|
|
1526
1808
|
});
|
|
1527
|
-
image.clipPath = state.originalClip ? (await
|
|
1809
|
+
image.clipPath = state.originalClip ? (await import_fabric5.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1528
1810
|
restoreLocks(image, state.originalLocks);
|
|
1529
1811
|
image.setCoords();
|
|
1530
1812
|
}
|
|
@@ -1542,91 +1824,154 @@ function restoreLocks(obj, locks) {
|
|
|
1542
1824
|
}
|
|
1543
1825
|
|
|
1544
1826
|
// src/text-curve.ts
|
|
1545
|
-
var
|
|
1546
|
-
|
|
1827
|
+
var import_fabric6 = require("fabric");
|
|
1828
|
+
|
|
1829
|
+
// src/text-curve-geometry.ts
|
|
1830
|
+
var DEFAULT_TEXT_CURVE = {
|
|
1831
|
+
shape: "arc",
|
|
1832
|
+
arc: 0,
|
|
1833
|
+
wave: 0,
|
|
1834
|
+
waveLength: 4.1,
|
|
1835
|
+
offset: 0,
|
|
1836
|
+
centerOffset: 0
|
|
1837
|
+
};
|
|
1547
1838
|
var MIN_ARC = 0.5;
|
|
1548
1839
|
var FULL_CIRCLE_ARC = 99.5;
|
|
1549
1840
|
var MIN_SWEEP = 0.12;
|
|
1550
|
-
var WAVE_PERIOD_EM = 4.1;
|
|
1551
1841
|
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1842
|
+
var MIN_WAVE_LENGTH = 1;
|
|
1843
|
+
var MAX_WAVE_LENGTH = 12;
|
|
1552
1844
|
var WAVE_STEP = 6;
|
|
1553
|
-
var
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
return !!object && typeof object.text === "string";
|
|
1845
|
+
var MAX_WAVE_STEPS = 4e3;
|
|
1846
|
+
function round(value) {
|
|
1847
|
+
return Math.round(value * 100) / 100;
|
|
1557
1848
|
}
|
|
1558
|
-
function
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1849
|
+
function normalizeTextCurve(config) {
|
|
1850
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
1851
|
+
const wave = clamp(config.wave ?? 0, 0, 100);
|
|
1852
|
+
const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
|
|
1853
|
+
return {
|
|
1854
|
+
shape: config.shape ?? inferred,
|
|
1855
|
+
arc,
|
|
1856
|
+
wave,
|
|
1857
|
+
waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
|
|
1858
|
+
offset: clamp(config.offset ?? 0, -100, 100),
|
|
1859
|
+
centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
|
|
1860
|
+
};
|
|
1568
1861
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
const
|
|
1574
|
-
const
|
|
1862
|
+
function bendsBaseline(config) {
|
|
1863
|
+
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
1864
|
+
}
|
|
1865
|
+
function arcGeometry(width, config) {
|
|
1866
|
+
const magnitude = Math.min(100, Math.abs(config.arc));
|
|
1867
|
+
const direction = config.arc < 0 ? -1 : 1;
|
|
1868
|
+
const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1869
|
+
const baseRadius = width / baseSweep;
|
|
1870
|
+
const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
|
|
1871
|
+
const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
|
|
1872
|
+
return { radius, sweep, direction, baseRadius };
|
|
1873
|
+
}
|
|
1874
|
+
function arcLinePath(width, geometry, radius, rotation) {
|
|
1875
|
+
const { sweep, direction, baseRadius } = geometry;
|
|
1876
|
+
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
1575
1877
|
const centerX = width / 2;
|
|
1576
1878
|
const point = (angle) => [
|
|
1577
1879
|
centerX + radius * Math.sin(angle),
|
|
1578
|
-
direction * radius *
|
|
1880
|
+
direction * (baseRadius - radius * Math.cos(angle))
|
|
1579
1881
|
];
|
|
1580
1882
|
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1581
1883
|
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1884
|
+
const r = round(radius);
|
|
1582
1885
|
if (full) {
|
|
1583
|
-
const start2 = point(-Math.PI);
|
|
1584
|
-
const
|
|
1886
|
+
const start2 = point(rotation - Math.PI);
|
|
1887
|
+
const opposite = point(rotation);
|
|
1585
1888
|
return {
|
|
1586
1889
|
data: [
|
|
1587
1890
|
`M ${format(start2)}`,
|
|
1588
|
-
`A ${
|
|
1589
|
-
`A ${
|
|
1891
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
1892
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1590
1893
|
].join(" "),
|
|
1591
|
-
length:
|
|
1894
|
+
length: radius * Math.PI * 2
|
|
1592
1895
|
};
|
|
1593
1896
|
}
|
|
1594
|
-
const start = point(-sweep / 2);
|
|
1595
|
-
const end = point(sweep / 2);
|
|
1897
|
+
const start = point(rotation - sweep / 2);
|
|
1898
|
+
const end = point(rotation + sweep / 2);
|
|
1596
1899
|
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1597
1900
|
return {
|
|
1598
|
-
data: `M ${format(start)} A ${
|
|
1599
|
-
|
|
1600
|
-
length: width
|
|
1901
|
+
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1902
|
+
length: radius * sweep
|
|
1601
1903
|
};
|
|
1602
1904
|
}
|
|
1603
|
-
function
|
|
1604
|
-
const amplitude =
|
|
1605
|
-
const period = Math.max(1, fontSize *
|
|
1606
|
-
const
|
|
1905
|
+
function waveLinePath(width, fontSize, config, dy) {
|
|
1906
|
+
const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
1907
|
+
const period = Math.max(1, fontSize * config.waveLength);
|
|
1908
|
+
const phase = config.offset / 100 * Math.PI * 2;
|
|
1909
|
+
const step = Math.min(WAVE_STEP, period / 8);
|
|
1910
|
+
const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
|
|
1607
1911
|
const commands = [];
|
|
1608
1912
|
let length = 0;
|
|
1609
1913
|
let previous = null;
|
|
1610
1914
|
for (let index = 0; index <= steps; index++) {
|
|
1611
1915
|
const x = width * index / steps;
|
|
1612
|
-
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1916
|
+
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
1613
1917
|
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1614
1918
|
previous = [x, y];
|
|
1615
1919
|
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1616
1920
|
}
|
|
1617
1921
|
return { data: commands.join(" "), length };
|
|
1618
1922
|
}
|
|
1619
|
-
function
|
|
1620
|
-
|
|
1923
|
+
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
1924
|
+
const curve = normalizeTextCurve(config);
|
|
1925
|
+
if (!bendsBaseline(curve)) return null;
|
|
1926
|
+
const lines = Math.max(1, Math.floor(lineCount));
|
|
1927
|
+
const paths = [];
|
|
1928
|
+
if (curve.shape === "wave") {
|
|
1929
|
+
for (let index = 0; index < lines; index++) {
|
|
1930
|
+
paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
1931
|
+
}
|
|
1932
|
+
return paths;
|
|
1933
|
+
}
|
|
1934
|
+
const geometry = arcGeometry(width, curve);
|
|
1935
|
+
const rotation = curve.offset / 100 * geometry.sweep;
|
|
1936
|
+
for (let index = 0; index < lines; index++) {
|
|
1937
|
+
const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
|
|
1938
|
+
paths.push(arcLinePath(width, geometry, radius, rotation));
|
|
1939
|
+
}
|
|
1940
|
+
return paths;
|
|
1621
1941
|
}
|
|
1622
1942
|
function buildCurvePathData(config, width, fontSize) {
|
|
1623
|
-
|
|
1624
|
-
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1625
|
-
return null;
|
|
1943
|
+
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
1626
1944
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1945
|
+
|
|
1946
|
+
// src/text-curve.ts
|
|
1947
|
+
var MEASURE_WIDTH = 1e5;
|
|
1948
|
+
var PATH_SLACK = 0.06;
|
|
1949
|
+
var FALLBACK_LINE_HEIGHT = 1.16;
|
|
1950
|
+
var patches = /* @__PURE__ */ new WeakMap();
|
|
1951
|
+
function isCurvable(object) {
|
|
1952
|
+
return !!object && typeof object.text === "string";
|
|
1953
|
+
}
|
|
1954
|
+
function measureText(text) {
|
|
1955
|
+
const authored = text.width;
|
|
1956
|
+
try {
|
|
1957
|
+
text.set({ width: MEASURE_WIDTH });
|
|
1958
|
+
text.initDimensions?.();
|
|
1959
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
1960
|
+
} finally {
|
|
1961
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
1962
|
+
text.initDimensions?.();
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function lineCountOf(text) {
|
|
1966
|
+
return Math.max(1, text._textLines?.length ?? 1);
|
|
1967
|
+
}
|
|
1968
|
+
function lineHeightOf(text) {
|
|
1969
|
+
const measured = text.getHeightOfLine?.(0);
|
|
1970
|
+
if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
|
|
1971
|
+
return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
|
|
1972
|
+
}
|
|
1973
|
+
function toFabricPath(curve) {
|
|
1974
|
+
return new import_fabric6.Path(curve.data, { visible: false, objectCaching: false });
|
|
1630
1975
|
}
|
|
1631
1976
|
var TextCurveManager = class {
|
|
1632
1977
|
constructor(canvas, layers, history, events) {
|
|
@@ -1643,33 +1988,35 @@ var TextCurveManager = class {
|
|
|
1643
1988
|
get(layerId) {
|
|
1644
1989
|
const layer = this.layers.get(layerId);
|
|
1645
1990
|
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1646
|
-
return layer.meta.curve ?? {
|
|
1991
|
+
return normalizeTextCurve(layer.meta.curve ?? {});
|
|
1647
1992
|
}
|
|
1648
1993
|
isCurved(layerId) {
|
|
1649
1994
|
const curve = this.get(layerId);
|
|
1650
|
-
return !!curve && (
|
|
1995
|
+
return !!curve && bendsBaseline(curve);
|
|
1651
1996
|
}
|
|
1652
|
-
/** Apply (or update) the curve on a text layer.
|
|
1997
|
+
/** Apply (or update) the curve on a text layer. A straight config clears it. */
|
|
1653
1998
|
apply(layerId, config, save = true) {
|
|
1654
1999
|
const layer = this.layers.get(layerId);
|
|
1655
2000
|
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1656
|
-
const next =
|
|
2001
|
+
const next = normalizeTextCurve(config);
|
|
1657
2002
|
const text = layer.fabricObject;
|
|
1658
2003
|
const run = measureText(text);
|
|
1659
|
-
|
|
1660
|
-
if (!curve) {
|
|
2004
|
+
if (!bendsBaseline(next)) {
|
|
1661
2005
|
this.detach(text, layer.meta.curveWidth);
|
|
1662
|
-
delete layer.meta.curve;
|
|
1663
2006
|
delete layer.meta.curveWidth;
|
|
2007
|
+
layer.meta.curve = next;
|
|
1664
2008
|
} else {
|
|
1665
2009
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1666
2010
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1667
|
-
text.
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2011
|
+
text.initDimensions?.();
|
|
2012
|
+
const curves = buildCurveLinePaths(
|
|
2013
|
+
next,
|
|
2014
|
+
run * (1 + PATH_SLACK),
|
|
2015
|
+
text.fontSize,
|
|
2016
|
+
lineCountOf(text),
|
|
2017
|
+
lineHeightOf(text)
|
|
2018
|
+
);
|
|
2019
|
+
if (curves) this.attach(text, curves);
|
|
1673
2020
|
layer.meta.curve = next;
|
|
1674
2021
|
}
|
|
1675
2022
|
text.initDimensions?.();
|
|
@@ -1682,12 +2029,14 @@ var TextCurveManager = class {
|
|
|
1682
2029
|
}
|
|
1683
2030
|
/** Remove the curve, restoring the authored text box width. */
|
|
1684
2031
|
clear(layerId, save = true) {
|
|
1685
|
-
|
|
2032
|
+
const layer = this.layers.get(layerId);
|
|
2033
|
+
const shape = layer?.meta.curve?.shape;
|
|
2034
|
+
return this.apply(layerId, { shape }, save);
|
|
1686
2035
|
}
|
|
1687
2036
|
/**
|
|
1688
|
-
* Rebuild the
|
|
1689
|
-
* font size all change the
|
|
1690
|
-
* without this the curve keeps
|
|
2037
|
+
* Rebuild the paths from the stored parameters. Text content, line count,
|
|
2038
|
+
* font family and font size all change the geometry the paths are sized to —
|
|
2039
|
+
* without this the curve keeps that of the text it was created from.
|
|
1691
2040
|
*/
|
|
1692
2041
|
refresh(layerId, save = false) {
|
|
1693
2042
|
const curve = this.layers.get(layerId)?.meta.curve;
|
|
@@ -1700,14 +2049,84 @@ var TextCurveManager = class {
|
|
|
1700
2049
|
if (layer.meta.curve) this.refresh(layer.id);
|
|
1701
2050
|
}
|
|
1702
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Puts every line's path on the object as one shape, and hooks per-line
|
|
2054
|
+
* measuring so each line is laid out on its own.
|
|
2055
|
+
*
|
|
2056
|
+
* The object's own path is the union of the lines: fabric sizes a curved text
|
|
2057
|
+
* object from it, and a single line's path would leave the rest of the block
|
|
2058
|
+
* outside the layer's box — where object caching clips it away.
|
|
2059
|
+
*/
|
|
2060
|
+
attach(text, curves) {
|
|
2061
|
+
const paths = curves.map(toFabricPath);
|
|
2062
|
+
const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
|
|
2063
|
+
text.set({
|
|
2064
|
+
path: union,
|
|
2065
|
+
pathAlign: "center",
|
|
2066
|
+
pathSide: "left",
|
|
2067
|
+
// Each line is centred on its own path by the measuring hook below, so
|
|
2068
|
+
// there is no single offset left to apply.
|
|
2069
|
+
pathStartOffset: 0
|
|
2070
|
+
});
|
|
2071
|
+
this.patchLineMeasure(text, paths, union);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Fabric lays every line of a text object along `this.path`, from one
|
|
2075
|
+
* `pathStartOffset` — so a multi-line run piles all its lines on top of each
|
|
2076
|
+
* other. Each line is measured against its own path instead, by swapping the
|
|
2077
|
+
* path in around fabric's own per-line measure pass.
|
|
2078
|
+
*
|
|
2079
|
+
* The paths all share the first one's `pathOffset`, which is the origin
|
|
2080
|
+
* fabric subtracts to place a glyph in the object — without that each line
|
|
2081
|
+
* would be re-centred on its own bounding box and land back in the same spot.
|
|
2082
|
+
*
|
|
2083
|
+
* `textAlign` is switched to centre for the pass, which is how fabric centres
|
|
2084
|
+
* a line on its path; the authored value is restored before anything else
|
|
2085
|
+
* reads it.
|
|
2086
|
+
*/
|
|
2087
|
+
patchLineMeasure(text, paths, union) {
|
|
2088
|
+
this.unpatchLineMeasure(text);
|
|
2089
|
+
const original = text._measureLine;
|
|
2090
|
+
if (typeof original !== "function") return;
|
|
2091
|
+
for (const path of paths) path.pathOffset = union.pathOffset;
|
|
2092
|
+
const installed = function(lineIndex) {
|
|
2093
|
+
const path = paths[lineIndex];
|
|
2094
|
+
if (!path) return original.call(this, lineIndex);
|
|
2095
|
+
const previousPath = this.path;
|
|
2096
|
+
const previousAlign = this.textAlign;
|
|
2097
|
+
this.path = path;
|
|
2098
|
+
this.textAlign = "center";
|
|
2099
|
+
this.setPathInfo?.();
|
|
2100
|
+
try {
|
|
2101
|
+
return original.call(this, lineIndex);
|
|
2102
|
+
} finally {
|
|
2103
|
+
this.path = previousPath;
|
|
2104
|
+
this.textAlign = previousAlign;
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "_measureLine");
|
|
2108
|
+
text._measureLine = installed;
|
|
2109
|
+
patches.set(text, { original, installed, ownedOriginal });
|
|
2110
|
+
}
|
|
2111
|
+
/** Restore fabric's own per-line measuring, if this manager replaced it. */
|
|
2112
|
+
unpatchLineMeasure(text) {
|
|
2113
|
+
const patch = patches.get(text);
|
|
2114
|
+
if (!patch) return;
|
|
2115
|
+
if (text._measureLine === patch.installed) {
|
|
2116
|
+
if (patch.ownedOriginal) text._measureLine = patch.original;
|
|
2117
|
+
else delete text._measureLine;
|
|
2118
|
+
}
|
|
2119
|
+
patches.delete(text);
|
|
2120
|
+
}
|
|
1703
2121
|
detach(text, authoredWidth) {
|
|
2122
|
+
this.unpatchLineMeasure(text);
|
|
1704
2123
|
text.set({ path: null, pathStartOffset: 0 });
|
|
1705
2124
|
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1706
2125
|
}
|
|
1707
2126
|
};
|
|
1708
2127
|
|
|
1709
2128
|
// src/mask-presets/manager.ts
|
|
1710
|
-
var
|
|
2129
|
+
var import_fabric7 = require("fabric");
|
|
1711
2130
|
|
|
1712
2131
|
// src/mask-presets/shapes.ts
|
|
1713
2132
|
var SHAPE_MASK_IDS = [
|
|
@@ -1974,13 +2393,13 @@ var MaskPresetManager = class {
|
|
|
1974
2393
|
objectCaching: false
|
|
1975
2394
|
};
|
|
1976
2395
|
if (isShapeMaskId(id)) {
|
|
1977
|
-
return new
|
|
2396
|
+
return new import_fabric7.Path(shapeMaskPathData(id), {
|
|
1978
2397
|
...shared,
|
|
1979
2398
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1980
2399
|
scaleY: height / SHAPE_MASK_BOX
|
|
1981
2400
|
});
|
|
1982
2401
|
}
|
|
1983
|
-
return new
|
|
2402
|
+
return new import_fabric7.FabricImage(renderTextureMask(id), {
|
|
1984
2403
|
...shared,
|
|
1985
2404
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1986
2405
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1989,7 +2408,7 @@ var MaskPresetManager = class {
|
|
|
1989
2408
|
};
|
|
1990
2409
|
|
|
1991
2410
|
// src/shadow.ts
|
|
1992
|
-
var
|
|
2411
|
+
var import_fabric8 = require("fabric");
|
|
1993
2412
|
|
|
1994
2413
|
// src/utils/color.ts
|
|
1995
2414
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -2027,7 +2446,7 @@ function applyLayerShadow(object, config) {
|
|
|
2027
2446
|
}
|
|
2028
2447
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
2029
2448
|
object.set({
|
|
2030
|
-
shadow: new
|
|
2449
|
+
shadow: new import_fabric8.Shadow({
|
|
2031
2450
|
color,
|
|
2032
2451
|
blur: Math.max(0, next.blur),
|
|
2033
2452
|
offsetX: next.offsetX,
|
|
@@ -2141,7 +2560,7 @@ var UnitConverter = class {
|
|
|
2141
2560
|
};
|
|
2142
2561
|
|
|
2143
2562
|
// src/serialization.ts
|
|
2144
|
-
var
|
|
2563
|
+
var import_fabric9 = require("fabric");
|
|
2145
2564
|
var VERSION = "2.0.0";
|
|
2146
2565
|
function serializeEditor(editor) {
|
|
2147
2566
|
return {
|
|
@@ -2177,7 +2596,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2177
2596
|
}
|
|
2178
2597
|
const staged = await Promise.all(
|
|
2179
2598
|
state.layers.map(async (serialized) => {
|
|
2180
|
-
const fabricObject = (await
|
|
2599
|
+
const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
2181
2600
|
if (!fabricObject) {
|
|
2182
2601
|
const source = serialized.fabricObject.src;
|
|
2183
2602
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2188,7 +2607,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2188
2607
|
return { serialized, fabricObject };
|
|
2189
2608
|
})
|
|
2190
2609
|
);
|
|
2191
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
2610
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
2192
2611
|
if (state.backgroundImage && !stagedBackground) {
|
|
2193
2612
|
const source = state.backgroundImage.src;
|
|
2194
2613
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2231,11 +2650,16 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
2231
2650
|
if (serialized.opacity !== 1) {
|
|
2232
2651
|
editor.layers.setOpacity(layer.id, serialized.opacity);
|
|
2233
2652
|
}
|
|
2653
|
+
const childData = serialized.children ?? serialized.meta?.groupChildren;
|
|
2654
|
+
if (layer.type === "group" && Array.isArray(childData)) {
|
|
2655
|
+
editor.layers.adoptGroupChildren(layer, childData);
|
|
2656
|
+
if (layer.meta.groupChildren) delete layer.meta.groupChildren;
|
|
2657
|
+
}
|
|
2234
2658
|
return layer;
|
|
2235
2659
|
}
|
|
2236
2660
|
|
|
2237
2661
|
// src/export.ts
|
|
2238
|
-
var
|
|
2662
|
+
var import_fabric10 = require("fabric");
|
|
2239
2663
|
|
|
2240
2664
|
// src/displacement.ts
|
|
2241
2665
|
var CHANNEL_INDEX = {
|
|
@@ -2329,7 +2753,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
2329
2753
|
}
|
|
2330
2754
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
2331
2755
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2332
|
-
const canvas = new
|
|
2756
|
+
const canvas = new import_fabric10.StaticCanvas(element, {
|
|
2333
2757
|
width: options.width ?? source.getWidth(),
|
|
2334
2758
|
height: options.height ?? source.getHeight(),
|
|
2335
2759
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -2359,7 +2783,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
2359
2783
|
throw new Error("Print area does not overlap the canvas");
|
|
2360
2784
|
}
|
|
2361
2785
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2362
|
-
const canvas = new
|
|
2786
|
+
const canvas = new import_fabric10.StaticCanvas(element, { width, height });
|
|
2363
2787
|
try {
|
|
2364
2788
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
2365
2789
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2799,7 +3223,7 @@ var ProjectManager = class {
|
|
|
2799
3223
|
};
|
|
2800
3224
|
|
|
2801
3225
|
// src/mask.ts
|
|
2802
|
-
var
|
|
3226
|
+
var import_fabric11 = require("fabric");
|
|
2803
3227
|
var MaskRefinementError = class extends Error {
|
|
2804
3228
|
constructor(code, message, cause) {
|
|
2805
3229
|
super(message);
|
|
@@ -2831,7 +3255,7 @@ var MaskController = class {
|
|
|
2831
3255
|
throw new Error("Mask dimensions must be positive integers");
|
|
2832
3256
|
}
|
|
2833
3257
|
const backing = this.makeCanvas(width, height);
|
|
2834
|
-
const image = new
|
|
3258
|
+
const image = new import_fabric11.FabricImage(backing, {
|
|
2835
3259
|
left: 0,
|
|
2836
3260
|
top: 0,
|
|
2837
3261
|
originX: "left",
|
|
@@ -3057,10 +3481,10 @@ var MaskController = class {
|
|
|
3057
3481
|
};
|
|
3058
3482
|
|
|
3059
3483
|
// src/masks/manager.ts
|
|
3060
|
-
var
|
|
3484
|
+
var import_fabric18 = require("fabric");
|
|
3061
3485
|
|
|
3062
3486
|
// src/masks/compose.ts
|
|
3063
|
-
var
|
|
3487
|
+
var import_fabric12 = require("fabric");
|
|
3064
3488
|
var MODE_OPERATION = {
|
|
3065
3489
|
add: "source-over",
|
|
3066
3490
|
subtract: "destination-out",
|
|
@@ -3070,7 +3494,7 @@ function neutralize(child) {
|
|
|
3070
3494
|
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
3071
3495
|
}
|
|
3072
3496
|
function baseRect(box) {
|
|
3073
|
-
return new
|
|
3497
|
+
return new import_fabric12.Rect({
|
|
3074
3498
|
left: box.left,
|
|
3075
3499
|
top: box.top,
|
|
3076
3500
|
width: Math.max(1, box.width),
|
|
@@ -3097,7 +3521,7 @@ function composeMaskGroup(children, entries, options) {
|
|
|
3097
3521
|
});
|
|
3098
3522
|
const first = entries.find((entry) => entry.visible);
|
|
3099
3523
|
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
3100
|
-
return new
|
|
3524
|
+
return new import_fabric12.Group(withBase, {
|
|
3101
3525
|
absolutePositioned: options.absolute,
|
|
3102
3526
|
// Cached, so the children's compositing operations resolve against each
|
|
3103
3527
|
// other instead of against the page underneath the mask.
|
|
@@ -3111,15 +3535,15 @@ function needsAbsoluteSpace(entries) {
|
|
|
3111
3535
|
}
|
|
3112
3536
|
|
|
3113
3537
|
// src/masks/edit.ts
|
|
3114
|
-
var
|
|
3538
|
+
var import_fabric14 = require("fabric");
|
|
3115
3539
|
|
|
3116
3540
|
// src/masks/space.ts
|
|
3117
|
-
var
|
|
3541
|
+
var import_fabric13 = require("fabric");
|
|
3118
3542
|
function matrixOf(object) {
|
|
3119
3543
|
return object.calcTransformMatrix();
|
|
3120
3544
|
}
|
|
3121
3545
|
function applyMatrix(object, matrix) {
|
|
3122
|
-
const decomposed =
|
|
3546
|
+
const decomposed = import_fabric13.util.qrDecompose(matrix);
|
|
3123
3547
|
object.set({
|
|
3124
3548
|
flipX: false,
|
|
3125
3549
|
flipY: false,
|
|
@@ -3136,19 +3560,19 @@ function applyMatrix(object, matrix) {
|
|
|
3136
3560
|
object.setCoords();
|
|
3137
3561
|
}
|
|
3138
3562
|
function toCanvasSpace(object, host) {
|
|
3139
|
-
applyMatrix(object,
|
|
3563
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
3140
3564
|
}
|
|
3141
3565
|
function toHostSpace(object, host) {
|
|
3142
3566
|
applyMatrix(
|
|
3143
3567
|
object,
|
|
3144
|
-
|
|
3568
|
+
import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
3145
3569
|
);
|
|
3146
3570
|
}
|
|
3147
3571
|
function relativeMatrix(object, host) {
|
|
3148
|
-
return
|
|
3572
|
+
return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
3149
3573
|
}
|
|
3150
3574
|
function applyRelativeMatrix(object, host, rel) {
|
|
3151
|
-
applyMatrix(object,
|
|
3575
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
3152
3576
|
}
|
|
3153
3577
|
function asObject(clip) {
|
|
3154
3578
|
return clip;
|
|
@@ -3261,8 +3685,8 @@ var MaskEditController = class {
|
|
|
3261
3685
|
if (!this.handle || !this.child || !this.group) return;
|
|
3262
3686
|
applyMatrix(
|
|
3263
3687
|
this.child,
|
|
3264
|
-
|
|
3265
|
-
|
|
3688
|
+
import_fabric14.util.multiplyTransformMatrices(
|
|
3689
|
+
import_fabric14.util.invertTransform(matrixOf(this.group)),
|
|
3266
3690
|
matrixOf(this.handle)
|
|
3267
3691
|
)
|
|
3268
3692
|
);
|
|
@@ -3273,15 +3697,15 @@ var MaskEditController = class {
|
|
|
3273
3697
|
};
|
|
3274
3698
|
|
|
3275
3699
|
// src/masks/store.ts
|
|
3276
|
-
var
|
|
3700
|
+
var import_fabric17 = require("fabric");
|
|
3277
3701
|
|
|
3278
3702
|
// src/masks/host.ts
|
|
3279
|
-
var
|
|
3703
|
+
var import_fabric15 = require("fabric");
|
|
3280
3704
|
function findCanvasHost(layers) {
|
|
3281
3705
|
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
3282
3706
|
}
|
|
3283
3707
|
function createCanvasHost(canvas, layers) {
|
|
3284
|
-
const rect = new
|
|
3708
|
+
const rect = new import_fabric15.Rect({
|
|
3285
3709
|
left: 0,
|
|
3286
3710
|
top: 0,
|
|
3287
3711
|
width: canvas.getWidth(),
|
|
@@ -3330,9 +3754,9 @@ function hostBoxOf(canvas, host, absolute) {
|
|
|
3330
3754
|
}
|
|
3331
3755
|
|
|
3332
3756
|
// src/masks/install.ts
|
|
3333
|
-
var
|
|
3757
|
+
var import_fabric16 = require("fabric");
|
|
3334
3758
|
function convertSpace(host, sources, absolute) {
|
|
3335
|
-
const wasAbsolute = host.clipPath instanceof
|
|
3759
|
+
const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
|
|
3336
3760
|
if (absolute === wasAbsolute) return;
|
|
3337
3761
|
for (const source of sources) {
|
|
3338
3762
|
if (absolute) toCanvasSpace(source, host);
|
|
@@ -3424,7 +3848,7 @@ var MaskStackStore = class {
|
|
|
3424
3848
|
const clip = host.clipPath;
|
|
3425
3849
|
if (!clip) return [];
|
|
3426
3850
|
const entries = this.list(target);
|
|
3427
|
-
if (entries.length === 0 || !(clip instanceof
|
|
3851
|
+
if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
|
|
3428
3852
|
const children = unwrapGroup(clip);
|
|
3429
3853
|
const extra = children.length - entries.length;
|
|
3430
3854
|
return extra > 0 ? children.slice(extra) : children;
|
|
@@ -3524,7 +3948,7 @@ var LayerMaskManager = class extends MaskStackStore {
|
|
|
3524
3948
|
if (index === -1) return false;
|
|
3525
3949
|
this.endEdit(false);
|
|
3526
3950
|
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3527
|
-
const group = host.clipPath instanceof
|
|
3951
|
+
const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
|
|
3528
3952
|
if (!group) return false;
|
|
3529
3953
|
const children = group.getObjects();
|
|
3530
3954
|
const child = children[children.length - entries.length + index];
|
|
@@ -3830,7 +4254,7 @@ var CanvasEditor = class {
|
|
|
3830
4254
|
const widthPx = this.units.toPixels(config.width);
|
|
3831
4255
|
const heightPx = this.units.toPixels(config.height);
|
|
3832
4256
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
3833
|
-
this.canvas = new
|
|
4257
|
+
this.canvas = new import_fabric19.Canvas(canvasElement, {
|
|
3834
4258
|
width: widthPx,
|
|
3835
4259
|
height: heightPx,
|
|
3836
4260
|
backgroundColor: this.designBackground,
|
|
@@ -3864,7 +4288,7 @@ var CanvasEditor = class {
|
|
|
3864
4288
|
// ─── Layer Operations ────────────────────────────────
|
|
3865
4289
|
async addImage(url, options) {
|
|
3866
4290
|
try {
|
|
3867
|
-
const img = await
|
|
4291
|
+
const img = await import_fabric19.FabricImage.fromURL(
|
|
3868
4292
|
url,
|
|
3869
4293
|
{},
|
|
3870
4294
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -3890,7 +4314,7 @@ var CanvasEditor = class {
|
|
|
3890
4314
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
3891
4315
|
const previous = layer.fabricObject;
|
|
3892
4316
|
try {
|
|
3893
|
-
const replacement = await
|
|
4317
|
+
const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
3894
4318
|
replacement.set({
|
|
3895
4319
|
left: previous.left,
|
|
3896
4320
|
top: previous.top,
|
|
@@ -3919,7 +4343,7 @@ var CanvasEditor = class {
|
|
|
3919
4343
|
}
|
|
3920
4344
|
}
|
|
3921
4345
|
addText(text, options) {
|
|
3922
|
-
const textbox = new
|
|
4346
|
+
const textbox = new import_fabric19.Textbox(text, {
|
|
3923
4347
|
fontSize: 32,
|
|
3924
4348
|
fontFamily: "Arial",
|
|
3925
4349
|
fill: "#000000",
|
|
@@ -3960,20 +4384,30 @@ var CanvasEditor = class {
|
|
|
3960
4384
|
return value === void 0 ? token : escapeXml(value);
|
|
3961
4385
|
})
|
|
3962
4386
|
);
|
|
3963
|
-
const
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4387
|
+
const specs = resolved.includes("<textPath") ? [textPathSpec(template, values)] : await explodeTemplateSvg(template, resolved, values);
|
|
4388
|
+
if (specs.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
4389
|
+
return this.history.transaction(async () => {
|
|
4390
|
+
const created = specs.map((spec) => {
|
|
4391
|
+
const layer2 = this.layers.add(spec.type, spec.object, spec.name);
|
|
4392
|
+
if (spec.curve) this.curves.apply(layer2.id, spec.curve, false);
|
|
4393
|
+
return layer2;
|
|
4394
|
+
});
|
|
4395
|
+
const layer = created.length === 1 ? created[0] : await this.groupLayers(
|
|
4396
|
+
created.map((l) => l.id),
|
|
4397
|
+
template.name
|
|
4398
|
+
) ?? created[0];
|
|
4399
|
+
layer.fabricObject.set({
|
|
4400
|
+
left: this.canvas.getWidth() / 2,
|
|
4401
|
+
top: this.canvas.getHeight() / 2,
|
|
4402
|
+
originX: "center",
|
|
4403
|
+
originY: "center"
|
|
4404
|
+
});
|
|
4405
|
+
layer.fabricObject.setCoords();
|
|
4406
|
+
this.layers.select(layer.id);
|
|
4407
|
+
this.canvas.requestRenderAll();
|
|
4408
|
+
this.history.save();
|
|
4409
|
+
return layer;
|
|
3972
4410
|
});
|
|
3973
|
-
const layer = this.layers.add("template", group, template.name);
|
|
3974
|
-
this.layers.select(layer.id);
|
|
3975
|
-
this.history.save();
|
|
3976
|
-
return layer;
|
|
3977
4411
|
}
|
|
3978
4412
|
removeLayer(id) {
|
|
3979
4413
|
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
@@ -3985,6 +4419,10 @@ var CanvasEditor = class {
|
|
|
3985
4419
|
selectLayer(id) {
|
|
3986
4420
|
this.layers.select(id);
|
|
3987
4421
|
}
|
|
4422
|
+
/** Select several layers at once — what a panel's shift/⌘ click drives. */
|
|
4423
|
+
selectLayers(ids) {
|
|
4424
|
+
this.layers.selectMany(ids);
|
|
4425
|
+
}
|
|
3988
4426
|
getSelectedLayer() {
|
|
3989
4427
|
const active = this.canvas.getActiveObject();
|
|
3990
4428
|
if (!active) return null;
|
|
@@ -4036,10 +4474,10 @@ var CanvasEditor = class {
|
|
|
4036
4474
|
const next = { ...previous, ...adjustments };
|
|
4037
4475
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
4038
4476
|
image.filters = [
|
|
4039
|
-
new
|
|
4040
|
-
new
|
|
4041
|
-
new
|
|
4042
|
-
new
|
|
4477
|
+
new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
4478
|
+
new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
4479
|
+
new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
4480
|
+
new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
4043
4481
|
];
|
|
4044
4482
|
layer.meta.imageAdjustments = next;
|
|
4045
4483
|
image.applyFilters();
|
|
@@ -4047,42 +4485,55 @@ var CanvasEditor = class {
|
|
|
4047
4485
|
this.history.save();
|
|
4048
4486
|
return true;
|
|
4049
4487
|
}
|
|
4050
|
-
/**
|
|
4488
|
+
/**
|
|
4489
|
+
* Combine two or more layers into one group layer. Picking a group among them
|
|
4490
|
+
* **absorbs** it — its children join the new group and the old group record is
|
|
4491
|
+
* dropped — because groups are exactly one level deep.
|
|
4492
|
+
*/
|
|
4051
4493
|
async groupLayers(ids, name = "Group") {
|
|
4052
4494
|
const uniqueIds = [...new Set(ids)];
|
|
4053
|
-
const
|
|
4054
|
-
if (
|
|
4495
|
+
const picked = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0 && layer.parentId === null);
|
|
4496
|
+
if (picked.length < 2 || picked.length !== uniqueIds.length) return null;
|
|
4497
|
+
const stack = this.layers.getAll();
|
|
4498
|
+
const topIndex = Math.max(...picked.map((layer) => stack.indexOf(layer)));
|
|
4499
|
+
const insertIndex = stack.slice(0, topIndex).filter((layer) => !picked.includes(layer)).length;
|
|
4055
4500
|
return this.history.transaction(() => {
|
|
4056
|
-
const
|
|
4057
|
-
const objects =
|
|
4058
|
-
for (const layer of
|
|
4059
|
-
|
|
4501
|
+
const children = [];
|
|
4502
|
+
const objects = [];
|
|
4503
|
+
for (const layer of picked) {
|
|
4504
|
+
if (layer.type === "group" && layer.children.length > 0) {
|
|
4505
|
+
const inner = layer.fabricObject.removeAll();
|
|
4506
|
+
for (const [index, object] of inner.entries()) {
|
|
4507
|
+
object.setCoords();
|
|
4508
|
+
const child = layer.children[index];
|
|
4509
|
+
if (!child) continue;
|
|
4510
|
+
children.push(child);
|
|
4511
|
+
objects.push(object);
|
|
4512
|
+
}
|
|
4513
|
+
layer.children = [];
|
|
4514
|
+
this.layers.detach(layer.id);
|
|
4515
|
+
} else {
|
|
4516
|
+
children.push(layer);
|
|
4517
|
+
objects.push(layer.fabricObject);
|
|
4518
|
+
this.layers.detach(layer.id);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
const group = new import_fabric19.Group(objects);
|
|
4060
4522
|
const grouped = this.layers.add("group", group, name);
|
|
4061
|
-
|
|
4523
|
+
for (const child of children) child.parentId = grouped.id;
|
|
4524
|
+
grouped.children = children;
|
|
4525
|
+
this.layers.reorder(grouped.id, insertIndex);
|
|
4062
4526
|
this.layers.select(grouped.id);
|
|
4063
4527
|
this.history.save();
|
|
4064
4528
|
return grouped;
|
|
4065
4529
|
});
|
|
4066
4530
|
}
|
|
4067
|
-
/**
|
|
4531
|
+
/** Split a group back into the layers it holds, in place. */
|
|
4068
4532
|
async ungroupLayer(id) {
|
|
4069
4533
|
const grouped = this.layers.get(id);
|
|
4070
|
-
|
|
4071
|
-
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
4072
|
-
const group = grouped.fabricObject;
|
|
4534
|
+
if (!grouped || grouped.type !== "group" || grouped.children.length === 0) return [];
|
|
4073
4535
|
return this.history.transaction(() => {
|
|
4074
|
-
const
|
|
4075
|
-
this.layers.remove(id);
|
|
4076
|
-
const restored = objects.map((object, index) => {
|
|
4077
|
-
object.setCoords();
|
|
4078
|
-
const data = childData[index];
|
|
4079
|
-
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
4080
|
-
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
4081
|
-
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
4082
|
-
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
4083
|
-
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
4084
|
-
return layer;
|
|
4085
|
-
});
|
|
4536
|
+
const restored = this.layers.explode(grouped);
|
|
4086
4537
|
this.history.save();
|
|
4087
4538
|
this.layers.select(restored[0]?.id ?? null);
|
|
4088
4539
|
return restored;
|
|
@@ -4130,7 +4581,7 @@ var CanvasEditor = class {
|
|
|
4130
4581
|
const layer = this.layers.get(id);
|
|
4131
4582
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
4132
4583
|
try {
|
|
4133
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4584
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
|
|
4134
4585
|
const image = await layer.fabricObject.clone();
|
|
4135
4586
|
image.set({
|
|
4136
4587
|
left: 0,
|
|
@@ -4406,7 +4857,7 @@ var CanvasEditor = class {
|
|
|
4406
4857
|
return;
|
|
4407
4858
|
}
|
|
4408
4859
|
try {
|
|
4409
|
-
const image = await
|
|
4860
|
+
const image = await import_fabric19.FabricImage.fromURL(
|
|
4410
4861
|
url,
|
|
4411
4862
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
4412
4863
|
{ originX: "left", originY: "top" }
|
|
@@ -4830,6 +5281,7 @@ var AnnotationOverlay = class {
|
|
|
4830
5281
|
applyLayerShadow,
|
|
4831
5282
|
applyObjectSelectionStyle,
|
|
4832
5283
|
applySelectionStyle,
|
|
5284
|
+
buildCurveLinePaths,
|
|
4833
5285
|
buildCurvePathData,
|
|
4834
5286
|
clamp,
|
|
4835
5287
|
clearTextureMaskCache,
|
|
@@ -4841,6 +5293,7 @@ var AnnotationOverlay = class {
|
|
|
4841
5293
|
displaceRgba,
|
|
4842
5294
|
drawTiles,
|
|
4843
5295
|
escapeXml,
|
|
5296
|
+
explodeTemplateSvg,
|
|
4844
5297
|
exportDataURL,
|
|
4845
5298
|
exportMockup,
|
|
4846
5299
|
exportPNG,
|
|
@@ -4853,6 +5306,7 @@ var AnnotationOverlay = class {
|
|
|
4853
5306
|
isShapeMaskId,
|
|
4854
5307
|
isTextureMaskId,
|
|
4855
5308
|
needsAbsoluteSpace,
|
|
5309
|
+
normalizeTextCurve,
|
|
4856
5310
|
readLayerShadow,
|
|
4857
5311
|
renderTextureMask,
|
|
4858
5312
|
resetTransform,
|
|
@@ -4861,6 +5315,8 @@ var AnnotationOverlay = class {
|
|
|
4861
5315
|
sanitizeSvg,
|
|
4862
5316
|
serializeEditor,
|
|
4863
5317
|
shapeMaskPathData,
|
|
5318
|
+
textPathCurve,
|
|
5319
|
+
textPathSpec,
|
|
4864
5320
|
toCanvasSpace,
|
|
4865
5321
|
toHostSpace,
|
|
4866
5322
|
unwrapGroup
|