@overtone-art/canvas-editor-core 0.3.2 → 0.5.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 +403 -48
- package/dist/index.d.ts +403 -48
- package/dist/index.global.js +55 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1352 -338
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1324 -314
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-Dh1unrzT.d.mts → types-JP8MZsIs.d.mts} +57 -22
- package/dist/{types-Dh1unrzT.d.ts → types-JP8MZsIs.d.ts} +57 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AnnotationOverlay: () => AnnotationOverlay,
|
|
24
|
+
CANVAS_MASK_TARGET: () => CANVAS_MASK_TARGET,
|
|
24
25
|
CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
|
|
25
26
|
CanvasEditor: () => CanvasEditor,
|
|
26
27
|
CropController: () => CropController,
|
|
@@ -33,6 +34,7 @@ __export(index_exports, {
|
|
|
33
34
|
HistoryManager: () => HistoryManager,
|
|
34
35
|
Layer: () => Layer,
|
|
35
36
|
LayerManager: () => LayerManager,
|
|
37
|
+
LayerMaskManager: () => LayerMaskManager,
|
|
36
38
|
LicenseManager: () => LicenseManager,
|
|
37
39
|
MaskController: () => MaskController,
|
|
38
40
|
MaskPresetManager: () => MaskPresetManager,
|
|
@@ -45,18 +47,16 @@ __export(index_exports, {
|
|
|
45
47
|
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
46
48
|
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
47
49
|
TextCurveManager: () => TextCurveManager,
|
|
50
|
+
TiledPatternObject: () => TiledPatternObject,
|
|
48
51
|
UnitConverter: () => UnitConverter,
|
|
49
52
|
applyAspectLock: () => applyAspectLock,
|
|
50
53
|
applyLayerShadow: () => applyLayerShadow,
|
|
51
54
|
applyObjectSelectionStyle: () => applyObjectSelectionStyle,
|
|
52
|
-
applyPatternLocks: () => applyPatternLocks,
|
|
53
55
|
applySelectionStyle: () => applySelectionStyle,
|
|
54
56
|
buildCurvePathData: () => buildCurvePathData,
|
|
55
|
-
buildPatternDataURL: () => buildPatternDataURL,
|
|
56
|
-
captureLocks: () => captureLocks,
|
|
57
57
|
clamp: () => clamp,
|
|
58
|
-
clearPatternImageCache: () => clearPatternImageCache,
|
|
59
58
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
59
|
+
composeMaskGroup: () => composeMaskGroup,
|
|
60
60
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
61
61
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
62
62
|
computeTilePositions: () => computeTilePositions,
|
|
@@ -69,12 +69,13 @@ __export(index_exports, {
|
|
|
69
69
|
exportPNG: () => exportPNG,
|
|
70
70
|
exportPrintArea: () => exportPrintArea,
|
|
71
71
|
exportSVG: () => exportSVG,
|
|
72
|
+
fitToBox: () => fitToBox,
|
|
72
73
|
generateId: () => generateId,
|
|
73
74
|
isCssColor: () => isCssColor,
|
|
74
75
|
isMaskPresetId: () => isMaskPresetId,
|
|
75
76
|
isShapeMaskId: () => isShapeMaskId,
|
|
76
77
|
isTextureMaskId: () => isTextureMaskId,
|
|
77
|
-
|
|
78
|
+
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
78
79
|
readLayerShadow: () => readLayerShadow,
|
|
79
80
|
renderTextureMask: () => renderTextureMask,
|
|
80
81
|
resetTransform: () => resetTransform,
|
|
@@ -82,12 +83,15 @@ __export(index_exports, {
|
|
|
82
83
|
round2: () => round2,
|
|
83
84
|
sanitizeSvg: () => sanitizeSvg,
|
|
84
85
|
serializeEditor: () => serializeEditor,
|
|
85
|
-
shapeMaskPathData: () => shapeMaskPathData
|
|
86
|
+
shapeMaskPathData: () => shapeMaskPathData,
|
|
87
|
+
toCanvasSpace: () => toCanvasSpace,
|
|
88
|
+
toHostSpace: () => toHostSpace,
|
|
89
|
+
unwrapGroup: () => unwrapGroup
|
|
86
90
|
});
|
|
87
91
|
module.exports = __toCommonJS(index_exports);
|
|
88
92
|
|
|
89
93
|
// src/editor.ts
|
|
90
|
-
var
|
|
94
|
+
var import_fabric17 = require("fabric");
|
|
91
95
|
|
|
92
96
|
// src/events.ts
|
|
93
97
|
var EventEmitter = class {
|
|
@@ -139,6 +143,12 @@ var Layer = class {
|
|
|
139
143
|
/** Non-fabric data (e.g. pattern config) that must persist with the layer. */
|
|
140
144
|
meta;
|
|
141
145
|
fabricObject;
|
|
146
|
+
/**
|
|
147
|
+
* Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
|
|
148
|
+
* part of the canvas but never part of the document: it is not serialized, and
|
|
149
|
+
* the layer's real object stays the one every editor API talks to.
|
|
150
|
+
*/
|
|
151
|
+
renderProxy;
|
|
142
152
|
constructor(type, fabricObject, name, id) {
|
|
143
153
|
this.id = id ?? generateId();
|
|
144
154
|
this.type = type;
|
|
@@ -148,6 +158,7 @@ var Layer = class {
|
|
|
148
158
|
this.opacity = 1;
|
|
149
159
|
this.meta = {};
|
|
150
160
|
this.fabricObject = fabricObject;
|
|
161
|
+
this.renderProxy = null;
|
|
151
162
|
this.fabricObject._layerId = this.id;
|
|
152
163
|
}
|
|
153
164
|
hasMeta() {
|
|
@@ -196,6 +207,7 @@ var LayerManager = class {
|
|
|
196
207
|
if (index === -1) return false;
|
|
197
208
|
const layer = this.layers[index];
|
|
198
209
|
this.canvas.remove(layer.fabricObject);
|
|
210
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
199
211
|
this.layers.splice(index, 1);
|
|
200
212
|
this.events.emit("layer:removed", { layerId: id });
|
|
201
213
|
this.emitChanged();
|
|
@@ -219,6 +231,10 @@ var LayerManager = class {
|
|
|
219
231
|
evented: !layer.locked
|
|
220
232
|
});
|
|
221
233
|
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
234
|
+
if (layer.renderProxy) {
|
|
235
|
+
fabricObject.set({ opacity: 0 });
|
|
236
|
+
this.syncZOrder();
|
|
237
|
+
}
|
|
222
238
|
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
223
239
|
this.canvas.requestRenderAll();
|
|
224
240
|
this.events.emit("layer:modified", { layerId: id });
|
|
@@ -226,6 +242,42 @@ var LayerManager = class {
|
|
|
226
242
|
this.onPropertyChanged?.();
|
|
227
243
|
return true;
|
|
228
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Attach (or clear) the object drawn in place of a layer's own object. The
|
|
247
|
+
* proxy tracks the layer's stacking position, visibility and opacity, and is
|
|
248
|
+
* removed with the layer — it must never outlive or drift from its source.
|
|
249
|
+
*/
|
|
250
|
+
setRenderProxy(id, proxy) {
|
|
251
|
+
const layer = this.get(id);
|
|
252
|
+
if (!layer || layer.renderProxy === proxy) return;
|
|
253
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
254
|
+
layer.renderProxy = proxy;
|
|
255
|
+
if (proxy) {
|
|
256
|
+
proxy._layerId = id;
|
|
257
|
+
proxy.set({
|
|
258
|
+
visible: layer.visible,
|
|
259
|
+
opacity: layer.opacity,
|
|
260
|
+
selectable: !layer.locked,
|
|
261
|
+
evented: !layer.locked
|
|
262
|
+
});
|
|
263
|
+
this.canvas.add(proxy);
|
|
264
|
+
this.syncZOrder();
|
|
265
|
+
}
|
|
266
|
+
this.canvas.requestRenderAll();
|
|
267
|
+
this.emitChanged();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Re-stack every canvas object to match layer order, keeping each proxy
|
|
271
|
+
* directly above the source it stands in for. Canvas indices can't be derived
|
|
272
|
+
* from layer indices once proxies are in the array, so the order is rebuilt
|
|
273
|
+
* front-to-back instead of computed.
|
|
274
|
+
*/
|
|
275
|
+
syncZOrder() {
|
|
276
|
+
for (const layer of this.layers) {
|
|
277
|
+
this.canvas.bringObjectToFront(layer.fabricObject);
|
|
278
|
+
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
229
281
|
reorder(id, newIndex) {
|
|
230
282
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
231
283
|
if (oldIndex === -1) return false;
|
|
@@ -234,9 +286,7 @@ var LayerManager = class {
|
|
|
234
286
|
if (oldIndex === clamped) return false;
|
|
235
287
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
236
288
|
this.layers.splice(clamped, 0, layer);
|
|
237
|
-
this.
|
|
238
|
-
this.canvas.moveObjectTo(l.fabricObject, i);
|
|
239
|
-
});
|
|
289
|
+
this.syncZOrder();
|
|
240
290
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
241
291
|
this.emitChanged();
|
|
242
292
|
this.onPropertyChanged?.();
|
|
@@ -248,7 +298,7 @@ var LayerManager = class {
|
|
|
248
298
|
} else {
|
|
249
299
|
const layer = this.get(id);
|
|
250
300
|
if (layer) {
|
|
251
|
-
this.canvas.setActiveObject(layer.fabricObject);
|
|
301
|
+
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
252
302
|
}
|
|
253
303
|
}
|
|
254
304
|
this.canvas.requestRenderAll();
|
|
@@ -273,6 +323,7 @@ var LayerManager = class {
|
|
|
273
323
|
if (layer.visible === visible) return;
|
|
274
324
|
layer.visible = visible;
|
|
275
325
|
layer.fabricObject.visible = visible;
|
|
326
|
+
if (layer.renderProxy) layer.renderProxy.visible = visible;
|
|
276
327
|
this.canvas.requestRenderAll();
|
|
277
328
|
this.emitChanged();
|
|
278
329
|
this.onPropertyChanged?.();
|
|
@@ -282,8 +333,9 @@ var LayerManager = class {
|
|
|
282
333
|
if (!layer) return;
|
|
283
334
|
if (layer.locked === locked) return;
|
|
284
335
|
layer.locked = locked;
|
|
285
|
-
layer.
|
|
286
|
-
|
|
336
|
+
const target = layer.renderProxy ?? layer.fabricObject;
|
|
337
|
+
target.selectable = !locked;
|
|
338
|
+
target.evented = !locked;
|
|
287
339
|
this.canvas.requestRenderAll();
|
|
288
340
|
this.emitChanged();
|
|
289
341
|
this.onPropertyChanged?.();
|
|
@@ -294,7 +346,8 @@ var LayerManager = class {
|
|
|
294
346
|
const next = Math.max(0, Math.min(1, opacity));
|
|
295
347
|
if (layer.opacity === next) return;
|
|
296
348
|
layer.opacity = next;
|
|
297
|
-
layer.
|
|
349
|
+
if (layer.renderProxy) layer.renderProxy.opacity = next;
|
|
350
|
+
else layer.fabricObject.opacity = next;
|
|
298
351
|
this.canvas.requestRenderAll();
|
|
299
352
|
this.emitChanged();
|
|
300
353
|
this.onPropertyChanged?.();
|
|
@@ -342,6 +395,7 @@ var LayerManager = class {
|
|
|
342
395
|
clear() {
|
|
343
396
|
for (const layer of this.layers) {
|
|
344
397
|
this.canvas.remove(layer.fabricObject);
|
|
398
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
345
399
|
}
|
|
346
400
|
this.layers = [];
|
|
347
401
|
this.emitChanged();
|
|
@@ -875,199 +929,536 @@ var CropController = class {
|
|
|
875
929
|
};
|
|
876
930
|
var STROKE2 = "#22c55e";
|
|
877
931
|
|
|
878
|
-
// src/pattern.ts
|
|
932
|
+
// src/pattern/pattern-manager.ts
|
|
933
|
+
var import_fabric3 = require("fabric");
|
|
934
|
+
|
|
935
|
+
// src/pattern/tiled-pattern-object.ts
|
|
879
936
|
var import_fabric2 = require("fabric");
|
|
937
|
+
|
|
938
|
+
// src/pattern/tile-geometry.ts
|
|
880
939
|
var MAX_TILES_PER_AXIS = 200;
|
|
940
|
+
function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
941
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
942
|
+
const radius = cornerRadius(anchor, targetW, targetH);
|
|
943
|
+
const span = radius * 2;
|
|
944
|
+
const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
|
|
945
|
+
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
946
|
+
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
947
|
+
const cols = Math.ceil(span / tileW) + 2;
|
|
948
|
+
const rows = Math.ceil(span / tileH) + 2;
|
|
949
|
+
const halfCols = Math.ceil(cols / 2);
|
|
950
|
+
const halfRows = Math.ceil(rows / 2);
|
|
951
|
+
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
952
|
+
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
953
|
+
const placements = [];
|
|
954
|
+
for (let j = -halfRows; j <= halfRows; j++) {
|
|
955
|
+
for (let i = -halfCols; i <= halfCols; i++) {
|
|
956
|
+
let x = i * tileW + shiftX;
|
|
957
|
+
let y = j * tileH + shiftY;
|
|
958
|
+
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
959
|
+
x += tileW * (config.horizontalOffset / 100);
|
|
960
|
+
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
961
|
+
y += tileH * (config.horizontalOffset / 100);
|
|
962
|
+
}
|
|
963
|
+
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
964
|
+
placements.push({ x, y, rotation });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return placements;
|
|
968
|
+
}
|
|
969
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
970
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
971
|
+
ctx.save();
|
|
972
|
+
ctx.translate(anchor.x, anchor.y);
|
|
973
|
+
ctx.rotate(config.angle * Math.PI / 180);
|
|
974
|
+
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
|
|
975
|
+
ctx.save();
|
|
976
|
+
ctx.translate(tile.x, tile.y);
|
|
977
|
+
ctx.rotate(tile.rotation);
|
|
978
|
+
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
979
|
+
ctx.restore();
|
|
980
|
+
}
|
|
981
|
+
ctx.restore();
|
|
982
|
+
}
|
|
983
|
+
function cornerRadius(origin, w, h) {
|
|
984
|
+
const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
|
|
985
|
+
const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
|
|
986
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
987
|
+
}
|
|
988
|
+
function clampOffset(value) {
|
|
989
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
990
|
+
return Math.max(-100, Math.min(100, value));
|
|
991
|
+
}
|
|
992
|
+
function mod2(n) {
|
|
993
|
+
return (n % 2 + 2) % 2;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/pattern/tiled-pattern-object.ts
|
|
997
|
+
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
998
|
+
var MAX_SNAPSHOT_SCALE = 8;
|
|
999
|
+
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1000
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
|
|
1001
|
+
static type = "TiledPattern";
|
|
1002
|
+
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
1003
|
+
source;
|
|
1004
|
+
config;
|
|
1005
|
+
/** The print area this pattern fills, in canvas units. */
|
|
1006
|
+
area;
|
|
1007
|
+
snapshotEl = null;
|
|
1008
|
+
snapshotScale = 0;
|
|
1009
|
+
constructor(source, config, area) {
|
|
1010
|
+
super({
|
|
1011
|
+
// Centre origin: the box tracks a tile, and a tile is placed by its centre.
|
|
1012
|
+
originX: "center",
|
|
1013
|
+
originY: "center",
|
|
1014
|
+
objectCaching: false,
|
|
1015
|
+
// Side handles would imply a non-uniform tile scale; the config has one.
|
|
1016
|
+
lockScalingFlip: true
|
|
1017
|
+
});
|
|
1018
|
+
this.source = source;
|
|
1019
|
+
this.config = config;
|
|
1020
|
+
this.area = area;
|
|
1021
|
+
this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
|
|
1022
|
+
this.syncBox();
|
|
1023
|
+
}
|
|
1024
|
+
/** Swap in a new config and re-fit the box to the tile it now describes. */
|
|
1025
|
+
setConfig(config) {
|
|
1026
|
+
this.config = config;
|
|
1027
|
+
this.syncBox();
|
|
1028
|
+
}
|
|
1029
|
+
/** Re-fit to a new print area (canvas resize). */
|
|
1030
|
+
setArea(area) {
|
|
1031
|
+
this.area = area;
|
|
1032
|
+
this.invalidate();
|
|
1033
|
+
this.syncBox();
|
|
1034
|
+
}
|
|
1035
|
+
/** Drop the cached source snapshot (the source was edited). */
|
|
1036
|
+
invalidate() {
|
|
1037
|
+
this.snapshotEl = null;
|
|
1038
|
+
this.snapshotScale = 0;
|
|
1039
|
+
this.dirty = true;
|
|
1040
|
+
}
|
|
1041
|
+
/** Free the offscreen snapshot. */
|
|
1042
|
+
dispose() {
|
|
1043
|
+
this.snapshotEl = null;
|
|
1044
|
+
this.snapshotScale = 0;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Put the box back on the anchor tile: the tile's size, the pattern angle, and
|
|
1048
|
+
* the grid origin displaced by the configured phase shift. Resets any live
|
|
1049
|
+
* gesture scale, so it must run only once that gesture has been folded in.
|
|
1050
|
+
*/
|
|
1051
|
+
syncBox() {
|
|
1052
|
+
const { tileW, tileH } = this.baseTile();
|
|
1053
|
+
const origin = this.source.getCenterPoint();
|
|
1054
|
+
const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
|
|
1055
|
+
this.set({
|
|
1056
|
+
left: anchor.x,
|
|
1057
|
+
top: anchor.y,
|
|
1058
|
+
width: tileW,
|
|
1059
|
+
height: tileH,
|
|
1060
|
+
scaleX: 1,
|
|
1061
|
+
scaleY: 1,
|
|
1062
|
+
angle: this.config.angle
|
|
1063
|
+
});
|
|
1064
|
+
this.setCoords();
|
|
1065
|
+
this.dirty = true;
|
|
1066
|
+
}
|
|
1067
|
+
/** Grid rotation in play right now — the live handle during a rotate gesture. */
|
|
1068
|
+
liveAngle() {
|
|
1069
|
+
return this.angle ?? 0;
|
|
1070
|
+
}
|
|
1071
|
+
/** Tile scale in play right now, as a config percentage. */
|
|
1072
|
+
liveScale() {
|
|
1073
|
+
return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Where the source's centre would have to sit for the box to stay put — i.e.
|
|
1077
|
+
* the grid origin implied by a drag. `PatternManager` moves the source there.
|
|
1078
|
+
*/
|
|
1079
|
+
liveOrigin() {
|
|
1080
|
+
const { tileW, tileH } = this.liveTile();
|
|
1081
|
+
const centre = this.getCenterPoint();
|
|
1082
|
+
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1083
|
+
return new import_fabric2.Point(centre.x - shift.x, centre.y - shift.y);
|
|
1084
|
+
}
|
|
1085
|
+
_render(ctx) {
|
|
1086
|
+
const { width, height } = this.area;
|
|
1087
|
+
if (width <= 0 || height <= 0) return;
|
|
1088
|
+
const { tileW, tileH } = this.liveTile();
|
|
1089
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
|
|
1090
|
+
if (!snapshot) return;
|
|
1091
|
+
const centre = this.getCenterPoint();
|
|
1092
|
+
const angle = this.liveAngle();
|
|
1093
|
+
ctx.save();
|
|
1094
|
+
ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
|
|
1095
|
+
ctx.rotate(-angle * Math.PI / 180);
|
|
1096
|
+
ctx.translate(-centre.x, -centre.y);
|
|
1097
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1098
|
+
const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
|
|
1099
|
+
const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
|
|
1100
|
+
drawTiles(ctx, snapshot, config, width, height, tileW, tileH, origin);
|
|
1101
|
+
ctx.restore();
|
|
1102
|
+
}
|
|
1103
|
+
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
1104
|
+
_toSVG() {
|
|
1105
|
+
const { width, height } = this.area;
|
|
1106
|
+
if (width <= 0 || height <= 0) return [];
|
|
1107
|
+
const el = document.createElement("canvas");
|
|
1108
|
+
el.width = Math.max(1, Math.round(width));
|
|
1109
|
+
el.height = Math.max(1, Math.round(height));
|
|
1110
|
+
const ctx = el.getContext("2d");
|
|
1111
|
+
if (!ctx) return [];
|
|
1112
|
+
const { tileW, tileH } = this.liveTile();
|
|
1113
|
+
const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
|
|
1114
|
+
if (!snapshot) return [];
|
|
1115
|
+
const centre = this.getCenterPoint();
|
|
1116
|
+
const angle = this.liveAngle();
|
|
1117
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1118
|
+
drawTiles(
|
|
1119
|
+
ctx,
|
|
1120
|
+
snapshot,
|
|
1121
|
+
{ ...this.config, angle, offsetX: 0, offsetY: 0 },
|
|
1122
|
+
width,
|
|
1123
|
+
height,
|
|
1124
|
+
tileW,
|
|
1125
|
+
tileH,
|
|
1126
|
+
{ x: centre.x - shift.x, y: centre.y - shift.y }
|
|
1127
|
+
);
|
|
1128
|
+
return [
|
|
1129
|
+
`<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
|
|
1130
|
+
`<image x="0" y="0" width="${width}" height="${height}" `,
|
|
1131
|
+
`xlink:href="${el.toDataURL("image/png")}"></image></g>
|
|
1132
|
+
`
|
|
1133
|
+
];
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1137
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1138
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1139
|
+
*/
|
|
1140
|
+
toObject() {
|
|
1141
|
+
const plain = super.toObject();
|
|
1142
|
+
delete plain.source;
|
|
1143
|
+
return plain;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
1147
|
+
* registry, which cannot carry a live source reference. Export paths clone
|
|
1148
|
+
* every canvas object, so without this a print export would silently lose the
|
|
1149
|
+
* tiling. The copy shares the source (it only ever reads from it).
|
|
1150
|
+
*/
|
|
1151
|
+
clone() {
|
|
1152
|
+
const copy = new _TiledPatternObject(this.source, this.config, this.area);
|
|
1153
|
+
copy.set({
|
|
1154
|
+
left: this.left,
|
|
1155
|
+
top: this.top,
|
|
1156
|
+
angle: this.angle,
|
|
1157
|
+
scaleX: this.scaleX,
|
|
1158
|
+
scaleY: this.scaleY,
|
|
1159
|
+
width: this.width,
|
|
1160
|
+
height: this.height,
|
|
1161
|
+
visible: this.visible,
|
|
1162
|
+
opacity: this.opacity
|
|
1163
|
+
});
|
|
1164
|
+
return Promise.resolve(copy);
|
|
1165
|
+
}
|
|
1166
|
+
/** The source's on-canvas width, before the tile scale. */
|
|
1167
|
+
baseW() {
|
|
1168
|
+
return Math.max(1, this.source.getBoundingRect().width);
|
|
1169
|
+
}
|
|
1170
|
+
/** Tile size from the config alone, ignoring any in-flight gesture. */
|
|
1171
|
+
baseTile() {
|
|
1172
|
+
const rect = this.source.getBoundingRect();
|
|
1173
|
+
const scale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1174
|
+
const floor = this.tileFloor();
|
|
1175
|
+
return {
|
|
1176
|
+
tileW: Math.max(floor, rect.width * scale),
|
|
1177
|
+
tileH: Math.max(floor, rect.height * scale)
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/** Tile size as drawn right now, including a live scale gesture. */
|
|
1181
|
+
liveTile() {
|
|
1182
|
+
const base = this.baseTile();
|
|
1183
|
+
const floor = this.tileFloor();
|
|
1184
|
+
return {
|
|
1185
|
+
tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
|
|
1186
|
+
tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
|
|
1190
|
+
tileFloor() {
|
|
1191
|
+
const { width, height } = this.area;
|
|
1192
|
+
return Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1193
|
+
}
|
|
1194
|
+
/** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
|
|
1195
|
+
shiftVector(tileW, tileH, angle) {
|
|
1196
|
+
const dx = tileW * (clampPercent(this.config.offsetX) / 100);
|
|
1197
|
+
const dy = tileH * (clampPercent(this.config.offsetY) / 100);
|
|
1198
|
+
const radians = angle * Math.PI / 180;
|
|
1199
|
+
const cos = Math.cos(radians);
|
|
1200
|
+
const sin = Math.sin(radians);
|
|
1201
|
+
return new import_fabric2.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1202
|
+
}
|
|
1203
|
+
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1204
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1205
|
+
return new import_fabric2.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
1209
|
+
* is still sharp enough and the source has not changed.
|
|
1210
|
+
*/
|
|
1211
|
+
ensureSnapshot(scale) {
|
|
1212
|
+
const wanted = this.clampScale(scale);
|
|
1213
|
+
const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
|
|
1214
|
+
if (!stale) return this.snapshotEl;
|
|
1215
|
+
const source = this.source;
|
|
1216
|
+
const opacity = source.opacity;
|
|
1217
|
+
source.opacity = 1;
|
|
1218
|
+
try {
|
|
1219
|
+
const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
|
|
1220
|
+
if (!el.width || !el.height) return null;
|
|
1221
|
+
this.snapshotEl = el;
|
|
1222
|
+
this.snapshotScale = wanted;
|
|
1223
|
+
} catch {
|
|
1224
|
+
return this.snapshotEl;
|
|
1225
|
+
} finally {
|
|
1226
|
+
source.opacity = opacity;
|
|
1227
|
+
source.dirty = false;
|
|
1228
|
+
}
|
|
1229
|
+
return this.snapshotEl;
|
|
1230
|
+
}
|
|
1231
|
+
/** Bound the snapshot by both a linear scale and a total pixel budget. */
|
|
1232
|
+
clampScale(scale) {
|
|
1233
|
+
const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
|
|
1234
|
+
const rect = this.source.getBoundingRect();
|
|
1235
|
+
const area = Math.max(1, rect.width * rect.height);
|
|
1236
|
+
const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
|
|
1237
|
+
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
function clampPercent(value) {
|
|
1241
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1242
|
+
return Math.max(-100, Math.min(100, value));
|
|
1243
|
+
}
|
|
1244
|
+
function contextScale(ctx) {
|
|
1245
|
+
if (typeof ctx.getTransform !== "function") return 1;
|
|
1246
|
+
try {
|
|
1247
|
+
const t = ctx.getTransform();
|
|
1248
|
+
return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
|
|
1249
|
+
} catch {
|
|
1250
|
+
return 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/pattern/pattern-manager.ts
|
|
1255
|
+
var MIN_TILE_SCALE = 10;
|
|
1256
|
+
var MAX_TILE_SCALE = 300;
|
|
881
1257
|
var PatternManager = class {
|
|
882
|
-
constructor(canvas, layers, history, events
|
|
1258
|
+
constructor(canvas, layers, history, events) {
|
|
883
1259
|
this.canvas = canvas;
|
|
884
1260
|
this.layers = layers;
|
|
885
1261
|
this.history = history;
|
|
886
1262
|
this.events = events;
|
|
887
|
-
this.
|
|
1263
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
1264
|
+
this.canvas.on("text:changed", this.onSourceModified);
|
|
1265
|
+
this.events.on("layer:removed", this.onLayerRemoved);
|
|
888
1266
|
}
|
|
889
1267
|
canvas;
|
|
890
1268
|
layers;
|
|
891
1269
|
history;
|
|
892
1270
|
events;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1271
|
+
proxies = /* @__PURE__ */ new Map();
|
|
1272
|
+
onObjectModified = (event) => {
|
|
1273
|
+
const target = event.target;
|
|
1274
|
+
if (target instanceof TiledPatternObject) this.commitGesture(target);
|
|
1275
|
+
else this.invalidateFor(target);
|
|
1276
|
+
};
|
|
1277
|
+
onSourceModified = (event) => {
|
|
1278
|
+
this.invalidateFor(event.target);
|
|
1279
|
+
};
|
|
1280
|
+
onLayerRemoved = ({ layerId }) => {
|
|
1281
|
+
const proxy = this.proxies.get(layerId);
|
|
1282
|
+
if (!proxy) return;
|
|
1283
|
+
proxy.dispose();
|
|
1284
|
+
this.proxies.delete(layerId);
|
|
1285
|
+
};
|
|
899
1286
|
isPattern(layerId) {
|
|
900
1287
|
return !!this.layers.get(layerId)?.meta.pattern;
|
|
901
1288
|
}
|
|
902
1289
|
getConfig(layerId) {
|
|
903
1290
|
return this.layers.get(layerId)?.meta.pattern?.config ?? null;
|
|
904
1291
|
}
|
|
905
|
-
/** Turn a
|
|
906
|
-
apply(layerId, config) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
config,
|
|
916
|
-
originalSrc: elementToDataURL(image) ?? image.getSrc(),
|
|
917
|
-
originalClip: clip ? clip.toObject() : null,
|
|
918
|
-
originalLocks: captureLocks(image),
|
|
919
|
-
original: {
|
|
920
|
-
left: image.left ?? 0,
|
|
921
|
-
top: image.top ?? 0,
|
|
922
|
-
scaleX: image.scaleX ?? 1,
|
|
923
|
-
scaleY: image.scaleY ?? 1,
|
|
924
|
-
width: image.width ?? 0,
|
|
925
|
-
height: image.height ?? 0,
|
|
926
|
-
angle: image.angle ?? 0,
|
|
927
|
-
cropX: image.cropX ?? 0,
|
|
928
|
-
cropY: image.cropY ?? 0
|
|
929
|
-
}
|
|
930
|
-
};
|
|
1292
|
+
/** Turn a layer into a repeating pattern, or update an existing one. */
|
|
1293
|
+
async apply(layerId, config) {
|
|
1294
|
+
const layer = this.layers.get(layerId);
|
|
1295
|
+
if (!layer) return;
|
|
1296
|
+
try {
|
|
1297
|
+
this.layers.setMeta(layerId, { pattern: { config } });
|
|
1298
|
+
const proxy = this.proxies.get(layerId);
|
|
1299
|
+
if (proxy) {
|
|
1300
|
+
proxy.setConfig(config);
|
|
1301
|
+
this.canvas.requestRenderAll();
|
|
931
1302
|
} else {
|
|
932
|
-
layer
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
await this.renderLayer(layer);
|
|
936
|
-
} catch (err) {
|
|
937
|
-
if (firstEnable) delete layer.meta.pattern;
|
|
938
|
-
throw err;
|
|
1303
|
+
this.attach(layer, config);
|
|
939
1304
|
}
|
|
940
1305
|
this.history.save();
|
|
941
|
-
}
|
|
942
|
-
this.events.emit("error", { message: "Failed to apply
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
this.events.emit("error", { message: "Failed to apply pattern", error });
|
|
943
1308
|
throw error;
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Stretch every restored pattern layer back over the full print area and
|
|
948
|
-
* re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
|
|
949
|
-
* it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
|
|
950
|
-
* after a state restore, where the canvas may be a different display size
|
|
951
|
-
* than when the pattern was baked — and to repair states saved while a
|
|
952
|
-
* pattern could still be dragged out of the print area.
|
|
953
|
-
*/
|
|
954
|
-
repinAll() {
|
|
955
|
-
const cw = this.canvas.getWidth();
|
|
956
|
-
const ch = this.canvas.getHeight();
|
|
957
|
-
let changed = false;
|
|
958
|
-
for (const layer of this.layers.getAll()) {
|
|
959
|
-
if (!layer.meta.pattern || layer.type !== "image") continue;
|
|
960
|
-
const image = layer.fabricObject;
|
|
961
|
-
image.set({
|
|
962
|
-
left: 0,
|
|
963
|
-
top: 0,
|
|
964
|
-
angle: 0,
|
|
965
|
-
scaleX: cw / (image.width || cw),
|
|
966
|
-
scaleY: ch / (image.height || ch)
|
|
967
|
-
});
|
|
968
|
-
applyPatternLocks(image);
|
|
969
|
-
image.setCoords();
|
|
970
|
-
changed = true;
|
|
971
1309
|
}
|
|
972
|
-
if (changed) this.canvas.requestRenderAll();
|
|
973
1310
|
}
|
|
974
|
-
/**
|
|
975
|
-
disable(layerId) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
scaleX: state.original.scaleX,
|
|
986
|
-
scaleY: state.original.scaleY,
|
|
987
|
-
width: state.original.width,
|
|
988
|
-
height: state.original.height,
|
|
989
|
-
cropX: state.original.cropX,
|
|
990
|
-
cropY: state.original.cropY,
|
|
991
|
-
angle: state.original.angle
|
|
1311
|
+
/** Drop the tiling and show the source again. */
|
|
1312
|
+
async disable(layerId) {
|
|
1313
|
+
const layer = this.layers.get(layerId);
|
|
1314
|
+
if (!layer?.meta.pattern) return;
|
|
1315
|
+
try {
|
|
1316
|
+
this.detach(layerId);
|
|
1317
|
+
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1318
|
+
layer.fabricObject.set({
|
|
1319
|
+
opacity: layer.opacity,
|
|
1320
|
+
selectable: !layer.locked,
|
|
1321
|
+
evented: !layer.locked
|
|
992
1322
|
});
|
|
993
|
-
|
|
994
|
-
restoreLocks(image, state.originalLocks);
|
|
995
|
-
image.setCoords();
|
|
996
|
-
delete layer.meta.pattern;
|
|
1323
|
+
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
997
1324
|
this.canvas.requestRenderAll();
|
|
998
1325
|
this.history.save();
|
|
999
|
-
}
|
|
1000
|
-
this.events.emit("error", { message: "Failed to clear
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
this.events.emit("error", { message: "Failed to clear pattern", error });
|
|
1001
1328
|
throw error;
|
|
1002
|
-
}
|
|
1329
|
+
}
|
|
1003
1330
|
}
|
|
1004
|
-
/**
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1331
|
+
/**
|
|
1332
|
+
* Rebuild every proxy after a state restore, migrating any layer that was
|
|
1333
|
+
* saved by the old bake-into-the-layer engine.
|
|
1334
|
+
*/
|
|
1335
|
+
async rehydrateAll() {
|
|
1336
|
+
this.clearProxies();
|
|
1337
|
+
for (const layer of this.layers.getAll()) {
|
|
1338
|
+
const state = layer.meta.pattern;
|
|
1339
|
+
if (!state) continue;
|
|
1340
|
+
try {
|
|
1341
|
+
if (isLegacyState(state)) {
|
|
1342
|
+
await unbakeLegacyLayer(layer, state);
|
|
1343
|
+
this.layers.setMeta(layer.id, { pattern: { config: state.config } });
|
|
1344
|
+
}
|
|
1345
|
+
this.attach(layer, state.config);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
this.events.emit("error", { message: "Failed to restore pattern layer", error });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
this.canvas.requestRenderAll();
|
|
1351
|
+
}
|
|
1352
|
+
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1353
|
+
syncArea() {
|
|
1354
|
+
const area = this.area();
|
|
1355
|
+
for (const proxy of this.proxies.values()) proxy.setArea(area);
|
|
1356
|
+
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1357
|
+
}
|
|
1358
|
+
/** Drop a layer's cached source snapshot (its content changed). */
|
|
1359
|
+
invalidate(layerId) {
|
|
1360
|
+
const proxy = this.proxies.get(layerId);
|
|
1361
|
+
if (!proxy) return;
|
|
1362
|
+
proxy.invalidate();
|
|
1363
|
+
proxy.syncBox();
|
|
1364
|
+
this.canvas.requestRenderAll();
|
|
1013
1365
|
}
|
|
1014
|
-
|
|
1366
|
+
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
1367
|
+
attachTo(layer) {
|
|
1015
1368
|
const state = layer.meta.pattern;
|
|
1016
|
-
if (!state) return;
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
);
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1369
|
+
if (!state || this.proxies.has(layer.id)) return;
|
|
1370
|
+
this.attach(layer, state.config);
|
|
1371
|
+
}
|
|
1372
|
+
dispose() {
|
|
1373
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
1374
|
+
this.canvas.off("text:changed", this.onSourceModified);
|
|
1375
|
+
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1376
|
+
this.clearProxies();
|
|
1377
|
+
}
|
|
1378
|
+
/** Release every proxy and the offscreen snapshot it holds. */
|
|
1379
|
+
clearProxies() {
|
|
1380
|
+
for (const proxy of this.proxies.values()) proxy.dispose();
|
|
1381
|
+
this.proxies.clear();
|
|
1382
|
+
}
|
|
1383
|
+
attach(layer, config) {
|
|
1384
|
+
const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
|
|
1385
|
+
const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
|
|
1386
|
+
layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
|
|
1387
|
+
this.proxies.set(layer.id, proxy);
|
|
1388
|
+
this.layers.setRenderProxy(layer.id, proxy);
|
|
1389
|
+
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1390
|
+
this.canvas.requestRenderAll();
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1394
|
+
* position becomes the grid origin (carried by the source), scale becomes the
|
|
1395
|
+
* tile scale, rotation becomes the pattern angle. Read the live transform
|
|
1396
|
+
* first — writing the config re-fits the box and destroys it.
|
|
1397
|
+
*/
|
|
1398
|
+
commitGesture(proxy) {
|
|
1399
|
+
const layer = this.layers.findByObject(proxy);
|
|
1400
|
+
const state = layer?.meta.pattern;
|
|
1401
|
+
if (!layer || !state) return;
|
|
1402
|
+
const origin = proxy.liveOrigin();
|
|
1403
|
+
const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
|
|
1404
|
+
const angle = normalizeAngle(proxy.liveAngle());
|
|
1405
|
+
layer.fabricObject.setPositionByOrigin(origin, "center", "center");
|
|
1406
|
+
layer.fabricObject.setCoords();
|
|
1407
|
+
const config = { ...state.config, scale, angle };
|
|
1408
|
+
this.layers.setMeta(layer.id, { pattern: { ...state, config } });
|
|
1409
|
+
proxy.setConfig(config);
|
|
1049
1410
|
this.canvas.requestRenderAll();
|
|
1411
|
+
this.history.save();
|
|
1412
|
+
}
|
|
1413
|
+
area() {
|
|
1414
|
+
return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
|
|
1415
|
+
}
|
|
1416
|
+
detach(layerId) {
|
|
1417
|
+
const proxy = this.proxies.get(layerId);
|
|
1418
|
+
if (!proxy) {
|
|
1419
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1423
|
+
proxy.dispose();
|
|
1424
|
+
this.proxies.delete(layerId);
|
|
1425
|
+
}
|
|
1426
|
+
invalidateFor(target) {
|
|
1427
|
+
if (!target) return;
|
|
1428
|
+
const layer = this.layers.findByObject(target);
|
|
1429
|
+
if (layer) this.invalidate(layer.id);
|
|
1050
1430
|
}
|
|
1051
1431
|
};
|
|
1052
|
-
function
|
|
1053
|
-
return
|
|
1054
|
-
|
|
1055
|
-
lockMovementY: obj.lockMovementY ?? false,
|
|
1056
|
-
lockScalingX: obj.lockScalingX ?? false,
|
|
1057
|
-
lockScalingY: obj.lockScalingY ?? false,
|
|
1058
|
-
lockRotation: obj.lockRotation ?? false,
|
|
1059
|
-
hasControls: obj.hasControls ?? true
|
|
1060
|
-
};
|
|
1432
|
+
function clamp2(value, min, max) {
|
|
1433
|
+
if (!Number.isFinite(value)) return min;
|
|
1434
|
+
return Math.max(min, Math.min(max, value));
|
|
1061
1435
|
}
|
|
1062
|
-
function
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1436
|
+
function normalizeAngle(angle) {
|
|
1437
|
+
if (!Number.isFinite(angle)) return 0;
|
|
1438
|
+
const wrapped = (angle % 360 + 360) % 360;
|
|
1439
|
+
return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
|
|
1440
|
+
}
|
|
1441
|
+
function isLegacyState(state) {
|
|
1442
|
+
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1443
|
+
}
|
|
1444
|
+
async function unbakeLegacyLayer(layer, state) {
|
|
1445
|
+
const image = layer.fabricObject;
|
|
1446
|
+
if (typeof image.setSrc !== "function" || !state.original) return;
|
|
1447
|
+
await image.setSrc(state.originalSrc);
|
|
1448
|
+
image.set({
|
|
1449
|
+
left: state.original.left,
|
|
1450
|
+
top: state.original.top,
|
|
1451
|
+
scaleX: state.original.scaleX,
|
|
1452
|
+
scaleY: state.original.scaleY,
|
|
1453
|
+
width: state.original.width,
|
|
1454
|
+
height: state.original.height,
|
|
1455
|
+
cropX: state.original.cropX,
|
|
1456
|
+
cropY: state.original.cropY,
|
|
1457
|
+
angle: state.original.angle
|
|
1070
1458
|
});
|
|
1459
|
+
image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1460
|
+
restoreLocks(image, state.originalLocks);
|
|
1461
|
+
image.setCoords();
|
|
1071
1462
|
}
|
|
1072
1463
|
function restoreLocks(obj, locks) {
|
|
1073
1464
|
obj.set(
|
|
@@ -1081,124 +1472,9 @@ function restoreLocks(obj, locks) {
|
|
|
1081
1472
|
}
|
|
1082
1473
|
);
|
|
1083
1474
|
}
|
|
1084
|
-
function elementToDataURL(image) {
|
|
1085
|
-
try {
|
|
1086
|
-
const el = image.getElement();
|
|
1087
|
-
const w = el.naturalWidth || el.width;
|
|
1088
|
-
const h = el.naturalHeight || el.height;
|
|
1089
|
-
if (!w || !h) return null;
|
|
1090
|
-
const off = document.createElement("canvas");
|
|
1091
|
-
off.width = w;
|
|
1092
|
-
off.height = h;
|
|
1093
|
-
const ctx = off.getContext("2d");
|
|
1094
|
-
if (!ctx) return null;
|
|
1095
|
-
ctx.drawImage(el, 0, 0);
|
|
1096
|
-
return off.toDataURL("image/png");
|
|
1097
|
-
} catch {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
var IMAGE_CACHE_MAX = 16;
|
|
1102
|
-
var imageCache = /* @__PURE__ */ new Map();
|
|
1103
|
-
function loadPatternImage(src, resolver) {
|
|
1104
|
-
const cached = imageCache.get(src);
|
|
1105
|
-
if (cached) {
|
|
1106
|
-
imageCache.delete(src);
|
|
1107
|
-
imageCache.set(src, cached);
|
|
1108
|
-
return cached;
|
|
1109
|
-
}
|
|
1110
|
-
const promise = decodeImage(src).catch(async (originalError) => {
|
|
1111
|
-
if (!resolver) throw originalError;
|
|
1112
|
-
const resolved = await resolver(src);
|
|
1113
|
-
if (!resolved || resolved === src) {
|
|
1114
|
-
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
1115
|
-
cause: originalError
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
return decodeImage(resolved);
|
|
1119
|
-
});
|
|
1120
|
-
promise.catch(() => {
|
|
1121
|
-
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
1122
|
-
});
|
|
1123
|
-
imageCache.set(src, promise);
|
|
1124
|
-
if (imageCache.size > IMAGE_CACHE_MAX) {
|
|
1125
|
-
const oldest = imageCache.keys().next().value;
|
|
1126
|
-
if (oldest !== void 0) imageCache.delete(oldest);
|
|
1127
|
-
}
|
|
1128
|
-
return promise;
|
|
1129
|
-
}
|
|
1130
|
-
function decodeImage(src) {
|
|
1131
|
-
return new Promise((resolve, reject) => {
|
|
1132
|
-
const img = new Image();
|
|
1133
|
-
img.crossOrigin = "anonymous";
|
|
1134
|
-
img.onload = () => resolve(img);
|
|
1135
|
-
img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
|
|
1136
|
-
img.src = src;
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
function clearPatternImageCache() {
|
|
1140
|
-
imageCache.clear();
|
|
1141
|
-
}
|
|
1142
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
1143
|
-
const img = await loadPatternImage(src, sourceResolver);
|
|
1144
|
-
const off = document.createElement("canvas");
|
|
1145
|
-
off.width = Math.max(1, Math.round(targetW));
|
|
1146
|
-
off.height = Math.max(1, Math.round(targetH));
|
|
1147
|
-
const ctx = off.getContext("2d");
|
|
1148
|
-
if (!ctx) return off.toDataURL("image/png");
|
|
1149
|
-
drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
|
|
1150
|
-
return off.toDataURL("image/png");
|
|
1151
|
-
}
|
|
1152
|
-
function computeTilePositions(config, targetW, targetH, baseW, baseH) {
|
|
1153
|
-
const diag = Math.sqrt(targetW * targetW + targetH * targetH);
|
|
1154
|
-
const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
|
|
1155
|
-
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
1156
|
-
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
1157
|
-
const cols = Math.ceil(diag / tileW) + 2;
|
|
1158
|
-
const rows = Math.ceil(diag / tileH) + 2;
|
|
1159
|
-
const halfCols = Math.ceil(cols / 2);
|
|
1160
|
-
const halfRows = Math.ceil(rows / 2);
|
|
1161
|
-
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
1162
|
-
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
1163
|
-
const placements = [];
|
|
1164
|
-
for (let j = -halfRows; j <= halfRows; j++) {
|
|
1165
|
-
for (let i = -halfCols; i <= halfCols; i++) {
|
|
1166
|
-
let x = i * tileW + shiftX;
|
|
1167
|
-
let y = j * tileH + shiftY;
|
|
1168
|
-
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
1169
|
-
x += tileW * (config.horizontalOffset / 100);
|
|
1170
|
-
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
1171
|
-
y += tileH * (config.horizontalOffset / 100);
|
|
1172
|
-
}
|
|
1173
|
-
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
1174
|
-
placements.push({ x, y, rotation });
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
return placements;
|
|
1178
|
-
}
|
|
1179
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
|
|
1180
|
-
ctx.save();
|
|
1181
|
-
ctx.translate(targetW / 2, targetH / 2);
|
|
1182
|
-
ctx.rotate(config.angle * Math.PI / 180);
|
|
1183
|
-
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
|
|
1184
|
-
ctx.save();
|
|
1185
|
-
ctx.translate(tile.x, tile.y);
|
|
1186
|
-
ctx.rotate(tile.rotation);
|
|
1187
|
-
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
1188
|
-
ctx.restore();
|
|
1189
|
-
}
|
|
1190
|
-
ctx.restore();
|
|
1191
|
-
}
|
|
1192
|
-
function clampOffset(value) {
|
|
1193
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1194
|
-
return Math.max(-100, Math.min(100, value));
|
|
1195
|
-
}
|
|
1196
|
-
function mod2(n) {
|
|
1197
|
-
return (n % 2 + 2) % 2;
|
|
1198
|
-
}
|
|
1199
1475
|
|
|
1200
1476
|
// src/text-curve.ts
|
|
1201
|
-
var
|
|
1477
|
+
var import_fabric4 = require("fabric");
|
|
1202
1478
|
var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
|
|
1203
1479
|
var MIN_ARC = 0.5;
|
|
1204
1480
|
var FULL_CIRCLE_ARC = 99.5;
|
|
@@ -1321,7 +1597,7 @@ var TextCurveManager = class {
|
|
|
1321
1597
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1322
1598
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1323
1599
|
text.set({
|
|
1324
|
-
path: new
|
|
1600
|
+
path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
|
|
1325
1601
|
pathAlign: "center",
|
|
1326
1602
|
pathSide: "left",
|
|
1327
1603
|
pathStartOffset: Math.max(0, (curve.length - run) / 2)
|
|
@@ -1363,7 +1639,7 @@ var TextCurveManager = class {
|
|
|
1363
1639
|
};
|
|
1364
1640
|
|
|
1365
1641
|
// src/mask-presets/manager.ts
|
|
1366
|
-
var
|
|
1642
|
+
var import_fabric5 = require("fabric");
|
|
1367
1643
|
|
|
1368
1644
|
// src/mask-presets/shapes.ts
|
|
1369
1645
|
var SHAPE_MASK_IDS = [
|
|
@@ -1630,13 +1906,13 @@ var MaskPresetManager = class {
|
|
|
1630
1906
|
objectCaching: false
|
|
1631
1907
|
};
|
|
1632
1908
|
if (isShapeMaskId(id)) {
|
|
1633
|
-
return new
|
|
1909
|
+
return new import_fabric5.Path(shapeMaskPathData(id), {
|
|
1634
1910
|
...shared,
|
|
1635
1911
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1636
1912
|
scaleY: height / SHAPE_MASK_BOX
|
|
1637
1913
|
});
|
|
1638
1914
|
}
|
|
1639
|
-
return new
|
|
1915
|
+
return new import_fabric5.FabricImage(renderTextureMask(id), {
|
|
1640
1916
|
...shared,
|
|
1641
1917
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1642
1918
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1645,7 +1921,7 @@ var MaskPresetManager = class {
|
|
|
1645
1921
|
};
|
|
1646
1922
|
|
|
1647
1923
|
// src/shadow.ts
|
|
1648
|
-
var
|
|
1924
|
+
var import_fabric6 = require("fabric");
|
|
1649
1925
|
|
|
1650
1926
|
// src/utils/color.ts
|
|
1651
1927
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -1683,7 +1959,7 @@ function applyLayerShadow(object, config) {
|
|
|
1683
1959
|
}
|
|
1684
1960
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1685
1961
|
object.set({
|
|
1686
|
-
shadow: new
|
|
1962
|
+
shadow: new import_fabric6.Shadow({
|
|
1687
1963
|
color,
|
|
1688
1964
|
blur: Math.max(0, next.blur),
|
|
1689
1965
|
offsetX: next.offsetX,
|
|
@@ -1797,7 +2073,7 @@ var UnitConverter = class {
|
|
|
1797
2073
|
};
|
|
1798
2074
|
|
|
1799
2075
|
// src/serialization.ts
|
|
1800
|
-
var
|
|
2076
|
+
var import_fabric7 = require("fabric");
|
|
1801
2077
|
var VERSION = "2.0.0";
|
|
1802
2078
|
function serializeEditor(editor) {
|
|
1803
2079
|
return {
|
|
@@ -1833,7 +2109,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1833
2109
|
}
|
|
1834
2110
|
const staged = await Promise.all(
|
|
1835
2111
|
state.layers.map(async (serialized) => {
|
|
1836
|
-
const fabricObject = (await
|
|
2112
|
+
const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1837
2113
|
if (!fabricObject) {
|
|
1838
2114
|
const source = serialized.fabricObject.src;
|
|
1839
2115
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1844,7 +2120,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1844
2120
|
return { serialized, fabricObject };
|
|
1845
2121
|
})
|
|
1846
2122
|
);
|
|
1847
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
2123
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1848
2124
|
if (state.backgroundImage && !stagedBackground) {
|
|
1849
2125
|
const source = state.backgroundImage.src;
|
|
1850
2126
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1891,7 +2167,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1891
2167
|
}
|
|
1892
2168
|
|
|
1893
2169
|
// src/export.ts
|
|
1894
|
-
var
|
|
2170
|
+
var import_fabric8 = require("fabric");
|
|
1895
2171
|
|
|
1896
2172
|
// src/displacement.ts
|
|
1897
2173
|
var CHANNEL_INDEX = {
|
|
@@ -1985,7 +2261,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1985
2261
|
}
|
|
1986
2262
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1987
2263
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1988
|
-
const canvas = new
|
|
2264
|
+
const canvas = new import_fabric8.StaticCanvas(element, {
|
|
1989
2265
|
width: options.width ?? source.getWidth(),
|
|
1990
2266
|
height: options.height ?? source.getHeight(),
|
|
1991
2267
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -2015,7 +2291,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
2015
2291
|
throw new Error("Print area does not overlap the canvas");
|
|
2016
2292
|
}
|
|
2017
2293
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2018
|
-
const canvas = new
|
|
2294
|
+
const canvas = new import_fabric8.StaticCanvas(element, { width, height });
|
|
2019
2295
|
try {
|
|
2020
2296
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
2021
2297
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2455,7 +2731,7 @@ var ProjectManager = class {
|
|
|
2455
2731
|
};
|
|
2456
2732
|
|
|
2457
2733
|
// src/mask.ts
|
|
2458
|
-
var
|
|
2734
|
+
var import_fabric9 = require("fabric");
|
|
2459
2735
|
var MaskRefinementError = class extends Error {
|
|
2460
2736
|
constructor(code, message, cause) {
|
|
2461
2737
|
super(message);
|
|
@@ -2487,7 +2763,7 @@ var MaskController = class {
|
|
|
2487
2763
|
throw new Error("Mask dimensions must be positive integers");
|
|
2488
2764
|
}
|
|
2489
2765
|
const backing = this.makeCanvas(width, height);
|
|
2490
|
-
const image = new
|
|
2766
|
+
const image = new import_fabric9.FabricImage(backing, {
|
|
2491
2767
|
left: 0,
|
|
2492
2768
|
top: 0,
|
|
2493
2769
|
originX: "left",
|
|
@@ -2712,6 +2988,737 @@ var MaskController = class {
|
|
|
2712
2988
|
}
|
|
2713
2989
|
};
|
|
2714
2990
|
|
|
2991
|
+
// src/masks/manager.ts
|
|
2992
|
+
var import_fabric16 = require("fabric");
|
|
2993
|
+
|
|
2994
|
+
// src/masks/compose.ts
|
|
2995
|
+
var import_fabric10 = require("fabric");
|
|
2996
|
+
var MODE_OPERATION = {
|
|
2997
|
+
add: "source-over",
|
|
2998
|
+
subtract: "destination-out",
|
|
2999
|
+
intersect: "destination-in"
|
|
3000
|
+
};
|
|
3001
|
+
function neutralize(child) {
|
|
3002
|
+
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
3003
|
+
}
|
|
3004
|
+
function baseRect(box) {
|
|
3005
|
+
return new import_fabric10.Rect({
|
|
3006
|
+
left: box.left,
|
|
3007
|
+
top: box.top,
|
|
3008
|
+
width: Math.max(1, box.width),
|
|
3009
|
+
height: Math.max(1, box.height),
|
|
3010
|
+
originX: "left",
|
|
3011
|
+
originY: "top",
|
|
3012
|
+
fill: "#000000",
|
|
3013
|
+
objectCaching: false
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
function composeMaskGroup(children, entries, options) {
|
|
3017
|
+
if (children.length === 0) return void 0;
|
|
3018
|
+
children.forEach((child, index) => {
|
|
3019
|
+
const entry = entries[index];
|
|
3020
|
+
child.set({ objectCaching: false });
|
|
3021
|
+
if (!entry || !entry.visible) {
|
|
3022
|
+
neutralize(child);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
child.set({
|
|
3026
|
+
opacity: entry.opacity,
|
|
3027
|
+
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
3028
|
+
});
|
|
3029
|
+
});
|
|
3030
|
+
const first = entries.find((entry) => entry.visible);
|
|
3031
|
+
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
3032
|
+
return new import_fabric10.Group(withBase, {
|
|
3033
|
+
absolutePositioned: options.absolute,
|
|
3034
|
+
// Cached, so the children's compositing operations resolve against each
|
|
3035
|
+
// other instead of against the page underneath the mask.
|
|
3036
|
+
objectCaching: true,
|
|
3037
|
+
subTargetCheck: false,
|
|
3038
|
+
interactive: false
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
function needsAbsoluteSpace(entries) {
|
|
3042
|
+
return entries.some((entry) => !entry.linked);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// src/masks/edit.ts
|
|
3046
|
+
var import_fabric12 = require("fabric");
|
|
3047
|
+
|
|
3048
|
+
// src/masks/space.ts
|
|
3049
|
+
var import_fabric11 = require("fabric");
|
|
3050
|
+
function matrixOf(object) {
|
|
3051
|
+
return object.calcTransformMatrix();
|
|
3052
|
+
}
|
|
3053
|
+
function applyMatrix(object, matrix) {
|
|
3054
|
+
const decomposed = import_fabric11.util.qrDecompose(matrix);
|
|
3055
|
+
object.set({
|
|
3056
|
+
flipX: false,
|
|
3057
|
+
flipY: false,
|
|
3058
|
+
originX: "center",
|
|
3059
|
+
originY: "center",
|
|
3060
|
+
left: decomposed.translateX,
|
|
3061
|
+
top: decomposed.translateY,
|
|
3062
|
+
scaleX: decomposed.scaleX,
|
|
3063
|
+
scaleY: decomposed.scaleY,
|
|
3064
|
+
angle: decomposed.angle,
|
|
3065
|
+
skewX: decomposed.skewX,
|
|
3066
|
+
skewY: 0
|
|
3067
|
+
});
|
|
3068
|
+
object.setCoords();
|
|
3069
|
+
}
|
|
3070
|
+
function toCanvasSpace(object, host) {
|
|
3071
|
+
applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
3072
|
+
}
|
|
3073
|
+
function toHostSpace(object, host) {
|
|
3074
|
+
applyMatrix(
|
|
3075
|
+
object,
|
|
3076
|
+
import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
function relativeMatrix(object, host) {
|
|
3080
|
+
return import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
3081
|
+
}
|
|
3082
|
+
function applyRelativeMatrix(object, host, rel) {
|
|
3083
|
+
applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
3084
|
+
}
|
|
3085
|
+
function asObject(clip) {
|
|
3086
|
+
return clip;
|
|
3087
|
+
}
|
|
3088
|
+
function toMatrix(values) {
|
|
3089
|
+
if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
|
|
3090
|
+
return null;
|
|
3091
|
+
return [values[0], values[1], values[2], values[3], values[4], values[5]];
|
|
3092
|
+
}
|
|
3093
|
+
function fitToBox(object, box, zoom = 1) {
|
|
3094
|
+
const width = Math.max(1, box.width) * zoom;
|
|
3095
|
+
const height = Math.max(1, box.height) * zoom;
|
|
3096
|
+
object.set({
|
|
3097
|
+
originX: "center",
|
|
3098
|
+
originY: "center",
|
|
3099
|
+
angle: 0,
|
|
3100
|
+
skewX: 0,
|
|
3101
|
+
skewY: 0,
|
|
3102
|
+
left: box.left + box.width / 2,
|
|
3103
|
+
top: box.top + box.height / 2,
|
|
3104
|
+
scaleX: width / Math.max(1, object.width ?? 1),
|
|
3105
|
+
scaleY: height / Math.max(1, object.height ?? 1)
|
|
3106
|
+
});
|
|
3107
|
+
object.setCoords();
|
|
3108
|
+
}
|
|
3109
|
+
function unwrapGroup(group) {
|
|
3110
|
+
const children = group.removeAll();
|
|
3111
|
+
for (const child of children) child.setCoords();
|
|
3112
|
+
return children;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/masks/edit.ts
|
|
3116
|
+
var MaskEditController = class {
|
|
3117
|
+
constructor(canvas, onCommit) {
|
|
3118
|
+
this.canvas = canvas;
|
|
3119
|
+
this.onCommit = onCommit;
|
|
3120
|
+
}
|
|
3121
|
+
canvas;
|
|
3122
|
+
onCommit;
|
|
3123
|
+
handle = null;
|
|
3124
|
+
editing = null;
|
|
3125
|
+
child = null;
|
|
3126
|
+
group = null;
|
|
3127
|
+
active() {
|
|
3128
|
+
return this.editing;
|
|
3129
|
+
}
|
|
3130
|
+
/** Put a handle on the canvas for `child`, an object inside `group`. */
|
|
3131
|
+
async begin(edit, group, child) {
|
|
3132
|
+
this.end();
|
|
3133
|
+
const handle = await child.clone();
|
|
3134
|
+
applyMatrix(handle, matrixOf(child));
|
|
3135
|
+
handle.set({
|
|
3136
|
+
// Outline only: the mask's own fill would sit over the artwork it is
|
|
3137
|
+
// supposed to be revealing.
|
|
3138
|
+
fill: "rgba(0,0,0,0.001)",
|
|
3139
|
+
stroke: "#e0b055",
|
|
3140
|
+
strokeWidth: 2,
|
|
3141
|
+
strokeDashArray: [6, 4],
|
|
3142
|
+
strokeUniform: true,
|
|
3143
|
+
opacity: 1,
|
|
3144
|
+
selectable: true,
|
|
3145
|
+
evented: true,
|
|
3146
|
+
hasControls: true,
|
|
3147
|
+
hasBorders: true,
|
|
3148
|
+
objectCaching: false,
|
|
3149
|
+
excludeFromExport: true
|
|
3150
|
+
});
|
|
3151
|
+
this.handle = handle;
|
|
3152
|
+
this.editing = edit;
|
|
3153
|
+
this.child = child;
|
|
3154
|
+
this.group = group;
|
|
3155
|
+
this.canvas.add(handle);
|
|
3156
|
+
this.canvas.setActiveObject(handle);
|
|
3157
|
+
this.canvas.on("object:moving", this.onTransform);
|
|
3158
|
+
this.canvas.on("object:scaling", this.onTransform);
|
|
3159
|
+
this.canvas.on("object:rotating", this.onTransform);
|
|
3160
|
+
this.canvas.on("object:modified", this.onModified);
|
|
3161
|
+
this.canvas.requestRenderAll();
|
|
3162
|
+
return true;
|
|
3163
|
+
}
|
|
3164
|
+
/** Take the handle down. The geometry it wrote is already in the clip. */
|
|
3165
|
+
end() {
|
|
3166
|
+
if (!this.handle) return;
|
|
3167
|
+
this.canvas.off("object:moving", this.onTransform);
|
|
3168
|
+
this.canvas.off("object:scaling", this.onTransform);
|
|
3169
|
+
this.canvas.off("object:rotating", this.onTransform);
|
|
3170
|
+
this.canvas.off("object:modified", this.onModified);
|
|
3171
|
+
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
3172
|
+
this.canvas.remove(this.handle);
|
|
3173
|
+
this.handle = null;
|
|
3174
|
+
this.editing = null;
|
|
3175
|
+
this.child = null;
|
|
3176
|
+
this.group = null;
|
|
3177
|
+
this.canvas.requestRenderAll();
|
|
3178
|
+
}
|
|
3179
|
+
dispose() {
|
|
3180
|
+
this.end();
|
|
3181
|
+
}
|
|
3182
|
+
onTransform = (event) => {
|
|
3183
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
3184
|
+
this.write();
|
|
3185
|
+
};
|
|
3186
|
+
onModified = (event) => {
|
|
3187
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
3188
|
+
this.write();
|
|
3189
|
+
this.onCommit();
|
|
3190
|
+
};
|
|
3191
|
+
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
3192
|
+
write() {
|
|
3193
|
+
if (!this.handle || !this.child || !this.group) return;
|
|
3194
|
+
applyMatrix(
|
|
3195
|
+
this.child,
|
|
3196
|
+
import_fabric12.util.multiplyTransformMatrices(
|
|
3197
|
+
import_fabric12.util.invertTransform(matrixOf(this.group)),
|
|
3198
|
+
matrixOf(this.handle)
|
|
3199
|
+
)
|
|
3200
|
+
);
|
|
3201
|
+
this.group.dirty = true;
|
|
3202
|
+
this.group.set({ dirty: true });
|
|
3203
|
+
this.canvas.requestRenderAll();
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
|
|
3207
|
+
// src/masks/store.ts
|
|
3208
|
+
var import_fabric15 = require("fabric");
|
|
3209
|
+
|
|
3210
|
+
// src/masks/host.ts
|
|
3211
|
+
var import_fabric13 = require("fabric");
|
|
3212
|
+
function findCanvasHost(layers) {
|
|
3213
|
+
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
3214
|
+
}
|
|
3215
|
+
function createCanvasHost(canvas, layers) {
|
|
3216
|
+
const rect = new import_fabric13.Rect({
|
|
3217
|
+
left: 0,
|
|
3218
|
+
top: 0,
|
|
3219
|
+
width: canvas.getWidth(),
|
|
3220
|
+
height: canvas.getHeight(),
|
|
3221
|
+
originX: "left",
|
|
3222
|
+
originY: "top",
|
|
3223
|
+
fill: "#000000",
|
|
3224
|
+
globalCompositeOperation: "destination-in",
|
|
3225
|
+
// It is edited from the layer list, never on the canvas: a drag box on
|
|
3226
|
+
// something that cannot be dragged only reads as broken.
|
|
3227
|
+
selectable: false,
|
|
3228
|
+
evented: false,
|
|
3229
|
+
hasControls: false,
|
|
3230
|
+
hasBorders: false,
|
|
3231
|
+
objectCaching: false
|
|
3232
|
+
});
|
|
3233
|
+
const layer = layers.add("mask", rect, "Design mask");
|
|
3234
|
+
layer.meta.canvasMask = true;
|
|
3235
|
+
layers.setLocked(layer.id, true);
|
|
3236
|
+
return layer;
|
|
3237
|
+
}
|
|
3238
|
+
function pinCanvasHost(layers) {
|
|
3239
|
+
const all = layers.getAll();
|
|
3240
|
+
const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
|
|
3241
|
+
if (hostIndex === -1) return false;
|
|
3242
|
+
let lastContent = -1;
|
|
3243
|
+
all.forEach((layer, index) => {
|
|
3244
|
+
if (layer.type !== "mask") lastContent = index;
|
|
3245
|
+
});
|
|
3246
|
+
if (hostIndex >= lastContent) return false;
|
|
3247
|
+
layers.reorder(all[hostIndex].id, all.length - 1);
|
|
3248
|
+
return true;
|
|
3249
|
+
}
|
|
3250
|
+
function hostBoxOf(canvas, host, absolute) {
|
|
3251
|
+
if (!host) {
|
|
3252
|
+
return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
|
|
3253
|
+
}
|
|
3254
|
+
if (absolute) {
|
|
3255
|
+
host.setCoords();
|
|
3256
|
+
const rect = host.getBoundingRect();
|
|
3257
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
3258
|
+
}
|
|
3259
|
+
const width = Math.max(1, host.width ?? 1);
|
|
3260
|
+
const height = Math.max(1, host.height ?? 1);
|
|
3261
|
+
return { left: -width / 2, top: -height / 2, width, height };
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
// src/masks/install.ts
|
|
3265
|
+
var import_fabric14 = require("fabric");
|
|
3266
|
+
function convertSpace(host, sources, absolute) {
|
|
3267
|
+
const wasAbsolute = host.clipPath instanceof import_fabric14.Group ? host.clipPath.absolutePositioned : absolute;
|
|
3268
|
+
if (absolute === wasAbsolute) return;
|
|
3269
|
+
for (const source of sources) {
|
|
3270
|
+
if (absolute) toCanvasSpace(source, host);
|
|
3271
|
+
else toHostSpace(source, host);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function withRelativeTransforms(entries, sources, host, absolute) {
|
|
3275
|
+
return entries.map((entry, index) => {
|
|
3276
|
+
const source = sources[index];
|
|
3277
|
+
if (absolute && entry.linked && source) {
|
|
3278
|
+
return { ...entry, rel: [...relativeMatrix(source, host)] };
|
|
3279
|
+
}
|
|
3280
|
+
if (!entry.rel) return entry;
|
|
3281
|
+
const rest = { ...entry };
|
|
3282
|
+
delete rest.rel;
|
|
3283
|
+
return rest;
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
function installClip(host, entries, sources, box, absolute) {
|
|
3287
|
+
host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
|
|
3288
|
+
host.dirty = true;
|
|
3289
|
+
host.setCoords();
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// src/masks/store.ts
|
|
3293
|
+
var CANVAS_MASK_TARGET = "canvas";
|
|
3294
|
+
var DEFAULT_ENTRY = {
|
|
3295
|
+
mode: "add",
|
|
3296
|
+
linked: true,
|
|
3297
|
+
visible: true,
|
|
3298
|
+
opacity: 1
|
|
3299
|
+
};
|
|
3300
|
+
var MaskStackStore = class {
|
|
3301
|
+
constructor(canvas, layers, history, events) {
|
|
3302
|
+
this.canvas = canvas;
|
|
3303
|
+
this.layers = layers;
|
|
3304
|
+
this.history = history;
|
|
3305
|
+
this.events = events;
|
|
3306
|
+
}
|
|
3307
|
+
canvas;
|
|
3308
|
+
layers;
|
|
3309
|
+
history;
|
|
3310
|
+
events;
|
|
3311
|
+
selected = null;
|
|
3312
|
+
pinning = false;
|
|
3313
|
+
list(target) {
|
|
3314
|
+
return this.hostLayer(target)?.meta.maskStack ?? [];
|
|
3315
|
+
}
|
|
3316
|
+
get(target, maskId) {
|
|
3317
|
+
return this.list(target).find((entry) => entry.id === maskId);
|
|
3318
|
+
}
|
|
3319
|
+
/** Every target that currently carries at least one mask. */
|
|
3320
|
+
targets() {
|
|
3321
|
+
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3322
|
+
}
|
|
3323
|
+
/** The box a mask is fitted to, in the space the stack is composed in. */
|
|
3324
|
+
hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
|
|
3325
|
+
return hostBoxOf(this.canvas, this.host(target), absolute);
|
|
3326
|
+
}
|
|
3327
|
+
/** The host layer of a target, optionally creating the design overlay. */
|
|
3328
|
+
hostLayer(target, create = false) {
|
|
3329
|
+
if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
|
|
3330
|
+
const existing = findCanvasHost(this.layers);
|
|
3331
|
+
if (existing || !create) return existing;
|
|
3332
|
+
return createCanvasHost(this.canvas, this.layers);
|
|
3333
|
+
}
|
|
3334
|
+
host(target, create = false) {
|
|
3335
|
+
return this.hostLayer(target, create)?.fabricObject ?? null;
|
|
3336
|
+
}
|
|
3337
|
+
/** Re-pin the design overlay, guarding the reorder that re-triggers this. */
|
|
3338
|
+
pin() {
|
|
3339
|
+
if (this.pinning) return;
|
|
3340
|
+
this.pinning = true;
|
|
3341
|
+
try {
|
|
3342
|
+
pinCanvasHost(this.layers);
|
|
3343
|
+
} finally {
|
|
3344
|
+
this.pinning = false;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Take the current geometry back out of the composed clip, entry-aligned.
|
|
3349
|
+
*
|
|
3350
|
+
* The entry list is the authority on how to read the clip: with no entries the
|
|
3351
|
+
* clip predates the stack (a mask preset, or a single `clipPath` an older host
|
|
3352
|
+
* installed) and is one mask whole — including when it happens to be a group,
|
|
3353
|
+
* which is why this cannot just unwrap anything group-shaped.
|
|
3354
|
+
*/
|
|
3355
|
+
unwrap(target, host) {
|
|
3356
|
+
const clip = host.clipPath;
|
|
3357
|
+
if (!clip) return [];
|
|
3358
|
+
const entries = this.list(target);
|
|
3359
|
+
if (entries.length === 0 || !(clip instanceof import_fabric15.Group)) return [asObject(clip)];
|
|
3360
|
+
const children = unwrapGroup(clip);
|
|
3361
|
+
const extra = children.length - entries.length;
|
|
3362
|
+
return extra > 0 ? children.slice(extra) : children;
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
3366
|
+
* this, the first `add()` on an already-masked layer would compose a clip it
|
|
3367
|
+
* has no entry for and silently throw that mask away.
|
|
3368
|
+
*/
|
|
3369
|
+
entriesFor(target, sources) {
|
|
3370
|
+
const existing = this.list(target);
|
|
3371
|
+
if (existing.length > 0 || sources.length !== 1) return [...existing];
|
|
3372
|
+
const layer = this.hostLayer(target);
|
|
3373
|
+
const preset = layer?.meta.maskPreset;
|
|
3374
|
+
if (layer) {
|
|
3375
|
+
delete layer.meta.maskPreset;
|
|
3376
|
+
}
|
|
3377
|
+
return [
|
|
3378
|
+
{
|
|
3379
|
+
...DEFAULT_ENTRY,
|
|
3380
|
+
id: generateId(),
|
|
3381
|
+
name: typeof preset === "string" ? preset : "Mask",
|
|
3382
|
+
linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
|
|
3383
|
+
}
|
|
3384
|
+
];
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Install a stack: convert geometry into the space the stack needs, compose the
|
|
3388
|
+
* clip, store the entries, and commit one history checkpoint for the lot.
|
|
3389
|
+
*/
|
|
3390
|
+
commit(target, host, entries, sources, save = true, forceAbsolute = false) {
|
|
3391
|
+
const layer = this.hostLayer(target);
|
|
3392
|
+
if (!layer) return;
|
|
3393
|
+
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
3394
|
+
convertSpace(host, sources, absolute);
|
|
3395
|
+
const next = withRelativeTransforms(entries, sources, host, absolute);
|
|
3396
|
+
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
3397
|
+
if (next.length === 0) {
|
|
3398
|
+
delete layer.meta.maskStack;
|
|
3399
|
+
if (layer.meta.canvasMask) this.layers.remove(layer.id);
|
|
3400
|
+
} else {
|
|
3401
|
+
layer.meta.maskStack = next;
|
|
3402
|
+
}
|
|
3403
|
+
this.canvas.requestRenderAll();
|
|
3404
|
+
this.events.emit("masks:changed", { target });
|
|
3405
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
3406
|
+
if (save) this.history.save();
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
|
|
3410
|
+
// src/masks/manager.ts
|
|
3411
|
+
var LayerMaskManager = class extends MaskStackStore {
|
|
3412
|
+
// Committing mid-drag would recompose the group the handle writes into and
|
|
3413
|
+
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
3414
|
+
edits = new MaskEditController(this.canvas, () => this.history.save());
|
|
3415
|
+
onLayersChanged = () => this.pin();
|
|
3416
|
+
// Selecting a layer means the user has moved on from the mask they had open;
|
|
3417
|
+
// leaving both selected would show mask controls for an unrelated layer.
|
|
3418
|
+
onLayerSelected = () => {
|
|
3419
|
+
if (this.selected) this.select(null, null);
|
|
3420
|
+
};
|
|
3421
|
+
onObjectModified = (event) => {
|
|
3422
|
+
const object = event.target;
|
|
3423
|
+
if (!object) return;
|
|
3424
|
+
const layer = this.layers.findByObject(object);
|
|
3425
|
+
if (layer) this.reflow(layer.id);
|
|
3426
|
+
};
|
|
3427
|
+
constructor(canvas, layers, history, events) {
|
|
3428
|
+
super(canvas, layers, history, events);
|
|
3429
|
+
this.events.on("layers:changed", this.onLayersChanged);
|
|
3430
|
+
this.events.on("layer:selected", this.onLayerSelected);
|
|
3431
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
3432
|
+
}
|
|
3433
|
+
dispose() {
|
|
3434
|
+
this.edits.dispose();
|
|
3435
|
+
this.events.off("layers:changed", this.onLayersChanged);
|
|
3436
|
+
this.events.off("layer:selected", this.onLayerSelected);
|
|
3437
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
3438
|
+
this.selected = null;
|
|
3439
|
+
}
|
|
3440
|
+
// ─── Geometry editing ────────────────────────────────
|
|
3441
|
+
/** The mask currently being dragged on the canvas, if any. */
|
|
3442
|
+
editing() {
|
|
3443
|
+
return this.edits.active();
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Put drag handles on one mask. The stack is composed in canvas space for the
|
|
3447
|
+
* duration — a linked mask would otherwise sit in the host's space, where the
|
|
3448
|
+
* handle's own canvas coordinates mean something else entirely. `endEdit`
|
|
3449
|
+
* returns it to whichever space its entries call for.
|
|
3450
|
+
*/
|
|
3451
|
+
async beginEdit(target, maskId) {
|
|
3452
|
+
const host = this.host(target);
|
|
3453
|
+
if (!host) return false;
|
|
3454
|
+
const entries = this.list(target);
|
|
3455
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3456
|
+
if (index === -1) return false;
|
|
3457
|
+
this.endEdit(false);
|
|
3458
|
+
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3459
|
+
const group = host.clipPath instanceof import_fabric16.Group ? host.clipPath : null;
|
|
3460
|
+
if (!group) return false;
|
|
3461
|
+
const children = group.getObjects();
|
|
3462
|
+
const child = children[children.length - entries.length + index];
|
|
3463
|
+
if (!child) return false;
|
|
3464
|
+
this.select(target, maskId);
|
|
3465
|
+
return this.edits.begin({ target, maskId }, group, child);
|
|
3466
|
+
}
|
|
3467
|
+
/** Take the handles down and settle the stack back into its own space. */
|
|
3468
|
+
endEdit(save = true) {
|
|
3469
|
+
const editing = this.edits.active();
|
|
3470
|
+
this.edits.end();
|
|
3471
|
+
if (editing) this.rebuild(editing.target, save);
|
|
3472
|
+
}
|
|
3473
|
+
// ─── Selection ───────────────────────────────────────
|
|
3474
|
+
getSelected() {
|
|
3475
|
+
return this.selected;
|
|
3476
|
+
}
|
|
3477
|
+
select(target, maskId) {
|
|
3478
|
+
this.selected = target && maskId ? { target, maskId } : null;
|
|
3479
|
+
this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
|
|
3480
|
+
}
|
|
3481
|
+
// ─── Mutations ───────────────────────────────────────
|
|
3482
|
+
add(target, object, options = {}) {
|
|
3483
|
+
const host = this.host(target, true);
|
|
3484
|
+
if (!host) return null;
|
|
3485
|
+
const sources = this.unwrap(target, host);
|
|
3486
|
+
const entries = this.entriesFor(target, sources);
|
|
3487
|
+
const entry = {
|
|
3488
|
+
...DEFAULT_ENTRY,
|
|
3489
|
+
id: generateId(),
|
|
3490
|
+
name: options.name ?? "Mask",
|
|
3491
|
+
...options.mode ? { mode: options.mode } : {},
|
|
3492
|
+
...options.linked === void 0 ? {} : { linked: options.linked },
|
|
3493
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3494
|
+
};
|
|
3495
|
+
if (target === CANVAS_MASK_TARGET) entry.linked = false;
|
|
3496
|
+
if (options.fit !== false) {
|
|
3497
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3498
|
+
}
|
|
3499
|
+
sources.push(object);
|
|
3500
|
+
entries.push(entry);
|
|
3501
|
+
this.commit(target, host, entries, sources);
|
|
3502
|
+
return entry;
|
|
3503
|
+
}
|
|
3504
|
+
/** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
|
|
3505
|
+
replaceGeometry(target, maskId, object, options = {}) {
|
|
3506
|
+
const host = this.host(target);
|
|
3507
|
+
if (!host) return false;
|
|
3508
|
+
const entries = [...this.list(target)];
|
|
3509
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3510
|
+
if (index === -1) return false;
|
|
3511
|
+
const sources = this.unwrap(target, host);
|
|
3512
|
+
if (options.fit !== false) {
|
|
3513
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3514
|
+
}
|
|
3515
|
+
sources[index] = object;
|
|
3516
|
+
entries[index] = {
|
|
3517
|
+
...entries[index],
|
|
3518
|
+
...options.name ? { name: options.name } : {},
|
|
3519
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3520
|
+
};
|
|
3521
|
+
this.commit(target, host, entries, sources);
|
|
3522
|
+
return true;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
|
|
3526
|
+
* geometry it already has. This is what a zoom control drives: re-picking the
|
|
3527
|
+
* mask would cost another render of a generated alpha map just to resize it.
|
|
3528
|
+
*/
|
|
3529
|
+
refit(target, maskId, options = {}) {
|
|
3530
|
+
const host = this.host(target);
|
|
3531
|
+
if (!host) return false;
|
|
3532
|
+
const entries = [...this.list(target)];
|
|
3533
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3534
|
+
if (index === -1) return false;
|
|
3535
|
+
const sources = this.unwrap(target, host);
|
|
3536
|
+
const source = sources[index];
|
|
3537
|
+
if (!source) return false;
|
|
3538
|
+
fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3539
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
3540
|
+
return true;
|
|
3541
|
+
}
|
|
3542
|
+
remove(target, maskId) {
|
|
3543
|
+
const host = this.host(target);
|
|
3544
|
+
if (!host) return false;
|
|
3545
|
+
const entries = [...this.list(target)];
|
|
3546
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3547
|
+
if (index === -1) return false;
|
|
3548
|
+
const sources = this.unwrap(target, host);
|
|
3549
|
+
entries.splice(index, 1);
|
|
3550
|
+
sources.splice(index, 1);
|
|
3551
|
+
if (this.selected?.maskId === maskId) this.select(null, null);
|
|
3552
|
+
this.commit(target, host, entries, sources);
|
|
3553
|
+
return true;
|
|
3554
|
+
}
|
|
3555
|
+
clear(target) {
|
|
3556
|
+
const host = this.host(target);
|
|
3557
|
+
if (!host) return false;
|
|
3558
|
+
if (this.list(target).length === 0) return false;
|
|
3559
|
+
if (this.selected?.target === target) this.select(null, null);
|
|
3560
|
+
this.commit(target, host, [], []);
|
|
3561
|
+
return true;
|
|
3562
|
+
}
|
|
3563
|
+
reorder(target, maskId, index) {
|
|
3564
|
+
const host = this.host(target);
|
|
3565
|
+
if (!host) return false;
|
|
3566
|
+
const entries = [...this.list(target)];
|
|
3567
|
+
const from = entries.findIndex((entry2) => entry2.id === maskId);
|
|
3568
|
+
if (from === -1) return false;
|
|
3569
|
+
const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
|
|
3570
|
+
if (from === to) return false;
|
|
3571
|
+
const sources = this.unwrap(target, host);
|
|
3572
|
+
const [entry] = entries.splice(from, 1);
|
|
3573
|
+
const [source] = sources.splice(from, 1);
|
|
3574
|
+
entries.splice(to, 0, entry);
|
|
3575
|
+
sources.splice(to, 0, source);
|
|
3576
|
+
this.commit(target, host, entries, sources);
|
|
3577
|
+
return true;
|
|
3578
|
+
}
|
|
3579
|
+
/** Replace a mask's host metadata (which preset or generator produced it). */
|
|
3580
|
+
setMeta(target, maskId, meta) {
|
|
3581
|
+
return this.patch(target, maskId, () => ({ meta }));
|
|
3582
|
+
}
|
|
3583
|
+
setMode(target, maskId, mode) {
|
|
3584
|
+
return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
|
|
3585
|
+
}
|
|
3586
|
+
setVisible(target, maskId, visible) {
|
|
3587
|
+
return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
|
|
3588
|
+
}
|
|
3589
|
+
setOpacity(target, maskId, opacity) {
|
|
3590
|
+
if (!Number.isFinite(opacity)) return false;
|
|
3591
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
3592
|
+
return this.patch(
|
|
3593
|
+
target,
|
|
3594
|
+
maskId,
|
|
3595
|
+
(entry) => entry.opacity === next ? null : { opacity: next }
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
setName(target, maskId, name) {
|
|
3599
|
+
const trimmed = name.trim();
|
|
3600
|
+
if (!trimmed) return false;
|
|
3601
|
+
return this.patch(
|
|
3602
|
+
target,
|
|
3603
|
+
maskId,
|
|
3604
|
+
(entry) => entry.name === trimmed ? null : { name: trimmed }
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* Link or unlink one mask. Unlinking pins it where it currently appears;
|
|
3609
|
+
* re-linking records its position relative to the host so later host moves
|
|
3610
|
+
* carry it along.
|
|
3611
|
+
*/
|
|
3612
|
+
setLinked(target, maskId, linked) {
|
|
3613
|
+
if (target === CANVAS_MASK_TARGET) return false;
|
|
3614
|
+
const host = this.host(target);
|
|
3615
|
+
if (!host) return false;
|
|
3616
|
+
const entries = [...this.list(target)];
|
|
3617
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3618
|
+
if (index === -1 || entries[index].linked === linked) return false;
|
|
3619
|
+
const sources = this.unwrap(target, host);
|
|
3620
|
+
entries[index] = { ...entries[index], linked };
|
|
3621
|
+
this.commit(target, host, entries, sources);
|
|
3622
|
+
return true;
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Consume a layer, turning its artwork into a mask. Without an explicit target
|
|
3626
|
+
* it masks the layer directly beneath it, and the bottom layer masks the whole
|
|
3627
|
+
* design — there is nothing under it to clip.
|
|
3628
|
+
*/
|
|
3629
|
+
convertLayer(layerId, target) {
|
|
3630
|
+
const layer = this.layers.get(layerId);
|
|
3631
|
+
if (!layer || layer.meta.canvasMask) return null;
|
|
3632
|
+
const ordered = this.layers.getAll();
|
|
3633
|
+
const index = ordered.findIndex((candidate) => candidate.id === layerId);
|
|
3634
|
+
const below = index > 0 ? ordered[index - 1] : void 0;
|
|
3635
|
+
const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
|
|
3636
|
+
if (resolved === layerId) return null;
|
|
3637
|
+
const object = layer.fabricObject;
|
|
3638
|
+
if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
|
|
3639
|
+
this.history.beginTransaction();
|
|
3640
|
+
try {
|
|
3641
|
+
if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
|
|
3642
|
+
this.canvas.remove(object);
|
|
3643
|
+
this.layers.remove(layerId);
|
|
3644
|
+
const entry = this.add(resolved, object, { name: layer.name, fit: false });
|
|
3645
|
+
return entry ? { target: resolved, entry } : null;
|
|
3646
|
+
} finally {
|
|
3647
|
+
this.history.endTransaction();
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
// ─── Rebuild / restore ───────────────────────────────
|
|
3651
|
+
/** Recompose one stack's clip from the geometry it already holds. */
|
|
3652
|
+
rebuild(target, save = false) {
|
|
3653
|
+
const host = this.host(target);
|
|
3654
|
+
if (!host) return;
|
|
3655
|
+
const sources = this.unwrap(target, host);
|
|
3656
|
+
if (sources.length === 0) return;
|
|
3657
|
+
this.commit(target, host, this.entriesFor(target, sources), sources, save);
|
|
3658
|
+
}
|
|
3659
|
+
/** Re-derive linked masks after the host moved (canvas-space stacks only). */
|
|
3660
|
+
reflow(target) {
|
|
3661
|
+
const host = this.host(target);
|
|
3662
|
+
if (!host) return;
|
|
3663
|
+
const entries = this.list(target);
|
|
3664
|
+
if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
|
|
3665
|
+
if (!entries.some((entry) => entry.linked && entry.rel)) return;
|
|
3666
|
+
const sources = this.unwrap(target, host);
|
|
3667
|
+
entries.forEach((entry, index) => {
|
|
3668
|
+
const source = sources[index];
|
|
3669
|
+
const rel = toMatrix(entry.rel);
|
|
3670
|
+
if (!source || !entry.linked || !rel) return;
|
|
3671
|
+
applyRelativeMatrix(source, host, rel);
|
|
3672
|
+
});
|
|
3673
|
+
this.commit(target, host, [...entries], sources);
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Adopt a clip this manager did not install — a host's own single `clipPath` —
|
|
3677
|
+
* as a one-entry stack, so it shows up in the UI as a mask row before anything
|
|
3678
|
+
* is added to it. A mask preset is left alone unless asked for by name: it is
|
|
3679
|
+
* still owned by `MaskPresetManager` until a stack operation takes it over.
|
|
3680
|
+
*/
|
|
3681
|
+
adopt(target, name) {
|
|
3682
|
+
const layer = this.hostLayer(target);
|
|
3683
|
+
const clip = layer?.fabricObject.clipPath;
|
|
3684
|
+
if (!layer || !clip || layer.meta.pattern) return null;
|
|
3685
|
+
if ((layer.meta.maskStack ?? []).length > 0) return null;
|
|
3686
|
+
const entry = {
|
|
3687
|
+
...DEFAULT_ENTRY,
|
|
3688
|
+
id: generateId(),
|
|
3689
|
+
name: name ?? "Mask",
|
|
3690
|
+
linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
|
|
3691
|
+
};
|
|
3692
|
+
layer.meta.maskStack = [entry];
|
|
3693
|
+
delete layer.meta.maskPreset;
|
|
3694
|
+
this.rebuild(target);
|
|
3695
|
+
return entry;
|
|
3696
|
+
}
|
|
3697
|
+
/** After a state restore: re-pin the design overlay and recompose every stack. */
|
|
3698
|
+
refreshAll() {
|
|
3699
|
+
this.selected = null;
|
|
3700
|
+
this.pin();
|
|
3701
|
+
for (const layer of this.layers.getAll()) {
|
|
3702
|
+
if ((layer.meta.maskStack ?? []).length === 0) continue;
|
|
3703
|
+
this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
// ─── Internals ───────────────────────────────────────
|
|
3707
|
+
patch(target, maskId, change) {
|
|
3708
|
+
const host = this.host(target);
|
|
3709
|
+
if (!host) return false;
|
|
3710
|
+
const entries = [...this.list(target)];
|
|
3711
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3712
|
+
if (index === -1) return false;
|
|
3713
|
+
const patch = change(entries[index]);
|
|
3714
|
+
if (!patch) return false;
|
|
3715
|
+
const sources = this.unwrap(target, host);
|
|
3716
|
+
entries[index] = { ...entries[index], ...patch };
|
|
3717
|
+
this.commit(target, host, entries, sources);
|
|
3718
|
+
return true;
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
|
|
2715
3722
|
// src/editor.ts
|
|
2716
3723
|
var MIN_ZOOM = 0.1;
|
|
2717
3724
|
var MAX_ZOOM = 8;
|
|
@@ -2729,6 +3736,8 @@ var CanvasEditor = class {
|
|
|
2729
3736
|
patterns;
|
|
2730
3737
|
curves;
|
|
2731
3738
|
maskPresets;
|
|
3739
|
+
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
3740
|
+
layerMasks;
|
|
2732
3741
|
fonts;
|
|
2733
3742
|
licensing;
|
|
2734
3743
|
pages;
|
|
@@ -2753,7 +3762,7 @@ var CanvasEditor = class {
|
|
|
2753
3762
|
const widthPx = this.units.toPixels(config.width);
|
|
2754
3763
|
const heightPx = this.units.toPixels(config.height);
|
|
2755
3764
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2756
|
-
this.canvas = new
|
|
3765
|
+
this.canvas = new import_fabric17.Canvas(canvasElement, {
|
|
2757
3766
|
width: widthPx,
|
|
2758
3767
|
height: heightPx,
|
|
2759
3768
|
backgroundColor: this.designBackground,
|
|
@@ -2774,15 +3783,10 @@ var CanvasEditor = class {
|
|
|
2774
3783
|
this.layers.setHistoryCallback(() => this.history.save());
|
|
2775
3784
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
2776
3785
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
2777
|
-
this.patterns = new PatternManager(
|
|
2778
|
-
this.canvas,
|
|
2779
|
-
this.layers,
|
|
2780
|
-
this.history,
|
|
2781
|
-
this.events,
|
|
2782
|
-
config.patternSourceResolver
|
|
2783
|
-
);
|
|
3786
|
+
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2784
3787
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2785
3788
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
3789
|
+
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
2786
3790
|
this.setupCanvasEvents();
|
|
2787
3791
|
this.refreshSelectionStyle();
|
|
2788
3792
|
this.history.saveImmediate();
|
|
@@ -2792,12 +3796,13 @@ var CanvasEditor = class {
|
|
|
2792
3796
|
// ─── Layer Operations ────────────────────────────────
|
|
2793
3797
|
async addImage(url, options) {
|
|
2794
3798
|
try {
|
|
2795
|
-
const img = await
|
|
3799
|
+
const img = await import_fabric17.FabricImage.fromURL(
|
|
2796
3800
|
url,
|
|
2797
3801
|
{},
|
|
2798
3802
|
{ originX: "left", originY: "top", ...options }
|
|
2799
3803
|
);
|
|
2800
3804
|
const layer = this.layers.add("image", img);
|
|
3805
|
+
this.layers.select(layer.id);
|
|
2801
3806
|
this.history.save();
|
|
2802
3807
|
return layer;
|
|
2803
3808
|
} catch (error) {
|
|
@@ -2817,7 +3822,7 @@ var CanvasEditor = class {
|
|
|
2817
3822
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2818
3823
|
const previous = layer.fabricObject;
|
|
2819
3824
|
try {
|
|
2820
|
-
const replacement = await
|
|
3825
|
+
const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2821
3826
|
replacement.set({
|
|
2822
3827
|
left: previous.left,
|
|
2823
3828
|
top: previous.top,
|
|
@@ -2846,7 +3851,7 @@ var CanvasEditor = class {
|
|
|
2846
3851
|
}
|
|
2847
3852
|
}
|
|
2848
3853
|
addText(text, options) {
|
|
2849
|
-
const textbox = new
|
|
3854
|
+
const textbox = new import_fabric17.Textbox(text, {
|
|
2850
3855
|
fontSize: 32,
|
|
2851
3856
|
fontFamily: "Arial",
|
|
2852
3857
|
fill: "#000000",
|
|
@@ -2857,12 +3862,14 @@ var CanvasEditor = class {
|
|
|
2857
3862
|
...options
|
|
2858
3863
|
});
|
|
2859
3864
|
const layer = this.layers.add("text", textbox);
|
|
3865
|
+
this.layers.select(layer.id);
|
|
2860
3866
|
this.history.save();
|
|
2861
3867
|
return layer;
|
|
2862
3868
|
}
|
|
2863
3869
|
addShape(plugin, options) {
|
|
2864
3870
|
const obj = plugin.create(options);
|
|
2865
3871
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
3872
|
+
this.layers.select(layer.id);
|
|
2866
3873
|
this.history.save();
|
|
2867
3874
|
return layer;
|
|
2868
3875
|
}
|
|
@@ -2885,10 +3892,10 @@ var CanvasEditor = class {
|
|
|
2885
3892
|
return value === void 0 ? token : escapeXml(value);
|
|
2886
3893
|
})
|
|
2887
3894
|
);
|
|
2888
|
-
const { objects, options } = await (0,
|
|
3895
|
+
const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
|
|
2889
3896
|
const validObjects = objects.filter((object) => object !== null);
|
|
2890
3897
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2891
|
-
const group =
|
|
3898
|
+
const group = import_fabric17.util.groupSVGElements(validObjects, options);
|
|
2892
3899
|
group.set({
|
|
2893
3900
|
left: this.canvas.getWidth() / 2,
|
|
2894
3901
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2896,6 +3903,7 @@ var CanvasEditor = class {
|
|
|
2896
3903
|
originY: "center"
|
|
2897
3904
|
});
|
|
2898
3905
|
const layer = this.layers.add("template", group, template.name);
|
|
3906
|
+
this.layers.select(layer.id);
|
|
2899
3907
|
this.history.save();
|
|
2900
3908
|
return layer;
|
|
2901
3909
|
}
|
|
@@ -2933,9 +3941,7 @@ var CanvasEditor = class {
|
|
|
2933
3941
|
const layer = this.layers.get(id);
|
|
2934
3942
|
if (!layer) return null;
|
|
2935
3943
|
const clone = await layer.fabricObject.clone();
|
|
2936
|
-
|
|
2937
|
-
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2938
|
-
}
|
|
3944
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2939
3945
|
clone.setCoords();
|
|
2940
3946
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
2941
3947
|
copy.meta = structuredClone(layer.meta);
|
|
@@ -2948,7 +3954,8 @@ var CanvasEditor = class {
|
|
|
2948
3954
|
evented: !layer.locked,
|
|
2949
3955
|
opacity: layer.opacity
|
|
2950
3956
|
});
|
|
2951
|
-
this.
|
|
3957
|
+
this.patterns.attachTo(copy);
|
|
3958
|
+
this.layers.select(copy.id);
|
|
2952
3959
|
this.canvas.requestRenderAll();
|
|
2953
3960
|
this.history.save();
|
|
2954
3961
|
return copy;
|
|
@@ -2961,10 +3968,10 @@ var CanvasEditor = class {
|
|
|
2961
3968
|
const next = { ...previous, ...adjustments };
|
|
2962
3969
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
2963
3970
|
image.filters = [
|
|
2964
|
-
new
|
|
2965
|
-
new
|
|
2966
|
-
new
|
|
2967
|
-
new
|
|
3971
|
+
new import_fabric17.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
3972
|
+
new import_fabric17.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
3973
|
+
new import_fabric17.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
3974
|
+
new import_fabric17.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
2968
3975
|
];
|
|
2969
3976
|
layer.meta.imageAdjustments = next;
|
|
2970
3977
|
image.applyFilters();
|
|
@@ -2981,7 +3988,7 @@ var CanvasEditor = class {
|
|
|
2981
3988
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2982
3989
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2983
3990
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2984
|
-
const group = new
|
|
3991
|
+
const group = new import_fabric17.Group(objects);
|
|
2985
3992
|
const grouped = this.layers.add("group", group, name);
|
|
2986
3993
|
grouped.meta.groupChildren = childData;
|
|
2987
3994
|
this.layers.select(grouped.id);
|
|
@@ -2996,11 +4003,9 @@ var CanvasEditor = class {
|
|
|
2996
4003
|
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
2997
4004
|
const group = grouped.fabricObject;
|
|
2998
4005
|
return this.history.transaction(() => {
|
|
2999
|
-
const transform = group.calcTransformMatrix();
|
|
3000
4006
|
const objects = group.removeAll();
|
|
3001
4007
|
this.layers.remove(id);
|
|
3002
4008
|
const restored = objects.map((object, index) => {
|
|
3003
|
-
import_fabric9.util.addTransformToObject(object, transform);
|
|
3004
4009
|
object.setCoords();
|
|
3005
4010
|
const data = childData[index];
|
|
3006
4011
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -3027,7 +4032,8 @@ var CanvasEditor = class {
|
|
|
3027
4032
|
}
|
|
3028
4033
|
try {
|
|
3029
4034
|
await deserializeEditor(this, state);
|
|
3030
|
-
this.patterns.
|
|
4035
|
+
await this.patterns.rehydrateAll();
|
|
4036
|
+
this.layerMasks.refreshAll();
|
|
3031
4037
|
} catch (error) {
|
|
3032
4038
|
if (!managedByHistory) {
|
|
3033
4039
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3056,7 +4062,7 @@ var CanvasEditor = class {
|
|
|
3056
4062
|
const layer = this.layers.get(id);
|
|
3057
4063
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
3058
4064
|
try {
|
|
3059
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4065
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
|
|
3060
4066
|
const image = await layer.fabricObject.clone();
|
|
3061
4067
|
image.set({
|
|
3062
4068
|
left: 0,
|
|
@@ -3076,7 +4082,11 @@ var CanvasEditor = class {
|
|
|
3076
4082
|
cloneObjects: false
|
|
3077
4083
|
});
|
|
3078
4084
|
}
|
|
3079
|
-
return await exportIsolatedPNG(
|
|
4085
|
+
return await exportIsolatedPNG(
|
|
4086
|
+
this.canvas,
|
|
4087
|
+
[layer.renderProxy ?? layer.fabricObject],
|
|
4088
|
+
options
|
|
4089
|
+
);
|
|
3080
4090
|
} catch (error) {
|
|
3081
4091
|
this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
|
|
3082
4092
|
throw error;
|
|
@@ -3328,7 +4338,7 @@ var CanvasEditor = class {
|
|
|
3328
4338
|
return;
|
|
3329
4339
|
}
|
|
3330
4340
|
try {
|
|
3331
|
-
const image = await
|
|
4341
|
+
const image = await import_fabric17.FabricImage.fromURL(
|
|
3332
4342
|
url,
|
|
3333
4343
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3334
4344
|
{ originX: "left", originY: "top" }
|
|
@@ -3381,7 +4391,6 @@ var CanvasEditor = class {
|
|
|
3381
4391
|
const sx = widthPx / oldWidth;
|
|
3382
4392
|
const sy = heightPx / oldHeight;
|
|
3383
4393
|
for (const layer of this.layers.getAll()) {
|
|
3384
|
-
if (layer.meta.pattern) continue;
|
|
3385
4394
|
const object = layer.fabricObject;
|
|
3386
4395
|
object.set({
|
|
3387
4396
|
left: (object.left ?? 0) * sx,
|
|
@@ -3402,7 +4411,7 @@ var CanvasEditor = class {
|
|
|
3402
4411
|
}
|
|
3403
4412
|
}
|
|
3404
4413
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
3405
|
-
this.patterns.
|
|
4414
|
+
this.patterns.syncArea();
|
|
3406
4415
|
this.canvas.requestRenderAll();
|
|
3407
4416
|
this.history.save();
|
|
3408
4417
|
this.events.emit("canvas:modified", {});
|
|
@@ -3597,10 +4606,11 @@ var CanvasEditor = class {
|
|
|
3597
4606
|
// ─── Cleanup ────────────────────────────────────────
|
|
3598
4607
|
dispose() {
|
|
3599
4608
|
this.masks.dispose();
|
|
4609
|
+
this.layerMasks.dispose();
|
|
3600
4610
|
this.snapping.dispose();
|
|
3601
4611
|
this.crop.dispose();
|
|
3602
4612
|
this.history.dispose();
|
|
3603
|
-
|
|
4613
|
+
this.patterns.dispose();
|
|
3604
4614
|
this.events.removeAllListeners();
|
|
3605
4615
|
this.canvas.dispose();
|
|
3606
4616
|
}
|
|
@@ -3720,6 +4730,7 @@ var AnnotationOverlay = class {
|
|
|
3720
4730
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3721
4731
|
0 && (module.exports = {
|
|
3722
4732
|
AnnotationOverlay,
|
|
4733
|
+
CANVAS_MASK_TARGET,
|
|
3723
4734
|
CANVAS_SIZE_PRESETS,
|
|
3724
4735
|
CanvasEditor,
|
|
3725
4736
|
CropController,
|
|
@@ -3732,6 +4743,7 @@ var AnnotationOverlay = class {
|
|
|
3732
4743
|
HistoryManager,
|
|
3733
4744
|
Layer,
|
|
3734
4745
|
LayerManager,
|
|
4746
|
+
LayerMaskManager,
|
|
3735
4747
|
LicenseManager,
|
|
3736
4748
|
MaskController,
|
|
3737
4749
|
MaskPresetManager,
|
|
@@ -3744,18 +4756,16 @@ var AnnotationOverlay = class {
|
|
|
3744
4756
|
TEXTURE_MASK_IDS,
|
|
3745
4757
|
TEXTURE_MASK_SIZE,
|
|
3746
4758
|
TextCurveManager,
|
|
4759
|
+
TiledPatternObject,
|
|
3747
4760
|
UnitConverter,
|
|
3748
4761
|
applyAspectLock,
|
|
3749
4762
|
applyLayerShadow,
|
|
3750
4763
|
applyObjectSelectionStyle,
|
|
3751
|
-
applyPatternLocks,
|
|
3752
4764
|
applySelectionStyle,
|
|
3753
4765
|
buildCurvePathData,
|
|
3754
|
-
buildPatternDataURL,
|
|
3755
|
-
captureLocks,
|
|
3756
4766
|
clamp,
|
|
3757
|
-
clearPatternImageCache,
|
|
3758
4767
|
clearTextureMaskCache,
|
|
4768
|
+
composeMaskGroup,
|
|
3759
4769
|
computeCoverPlacement,
|
|
3760
4770
|
computePrintAreaClip,
|
|
3761
4771
|
computeTilePositions,
|
|
@@ -3768,12 +4778,13 @@ var AnnotationOverlay = class {
|
|
|
3768
4778
|
exportPNG,
|
|
3769
4779
|
exportPrintArea,
|
|
3770
4780
|
exportSVG,
|
|
4781
|
+
fitToBox,
|
|
3771
4782
|
generateId,
|
|
3772
4783
|
isCssColor,
|
|
3773
4784
|
isMaskPresetId,
|
|
3774
4785
|
isShapeMaskId,
|
|
3775
4786
|
isTextureMaskId,
|
|
3776
|
-
|
|
4787
|
+
needsAbsoluteSpace,
|
|
3777
4788
|
readLayerShadow,
|
|
3778
4789
|
renderTextureMask,
|
|
3779
4790
|
resetTransform,
|
|
@@ -3781,6 +4792,9 @@ var AnnotationOverlay = class {
|
|
|
3781
4792
|
round2,
|
|
3782
4793
|
sanitizeSvg,
|
|
3783
4794
|
serializeEditor,
|
|
3784
|
-
shapeMaskPathData
|
|
4795
|
+
shapeMaskPathData,
|
|
4796
|
+
toCanvasSpace,
|
|
4797
|
+
toHostSpace,
|
|
4798
|
+
unwrapGroup
|
|
3785
4799
|
});
|
|
3786
4800
|
//# sourceMappingURL=index.js.map
|