@overtone-art/canvas-editor-core 0.4.0 → 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 +300 -26
- package/dist/index.d.ts +300 -26
- package/dist/index.global.js +53 -53
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +985 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +966 -92
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-BNzE_X5M.d.mts → types-JP8MZsIs.d.mts} +42 -1
- package/dist/{types-BNzE_X5M.d.ts → types-JP8MZsIs.d.ts} +42 -1
- 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,
|
|
@@ -54,6 +56,7 @@ __export(index_exports, {
|
|
|
54
56
|
buildCurvePathData: () => buildCurvePathData,
|
|
55
57
|
clamp: () => clamp,
|
|
56
58
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
59
|
+
composeMaskGroup: () => composeMaskGroup,
|
|
57
60
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
58
61
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
59
62
|
computeTilePositions: () => computeTilePositions,
|
|
@@ -66,11 +69,13 @@ __export(index_exports, {
|
|
|
66
69
|
exportPNG: () => exportPNG,
|
|
67
70
|
exportPrintArea: () => exportPrintArea,
|
|
68
71
|
exportSVG: () => exportSVG,
|
|
72
|
+
fitToBox: () => fitToBox,
|
|
69
73
|
generateId: () => generateId,
|
|
70
74
|
isCssColor: () => isCssColor,
|
|
71
75
|
isMaskPresetId: () => isMaskPresetId,
|
|
72
76
|
isShapeMaskId: () => isShapeMaskId,
|
|
73
77
|
isTextureMaskId: () => isTextureMaskId,
|
|
78
|
+
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
74
79
|
readLayerShadow: () => readLayerShadow,
|
|
75
80
|
renderTextureMask: () => renderTextureMask,
|
|
76
81
|
resetTransform: () => resetTransform,
|
|
@@ -78,12 +83,15 @@ __export(index_exports, {
|
|
|
78
83
|
round2: () => round2,
|
|
79
84
|
sanitizeSvg: () => sanitizeSvg,
|
|
80
85
|
serializeEditor: () => serializeEditor,
|
|
81
|
-
shapeMaskPathData: () => shapeMaskPathData
|
|
86
|
+
shapeMaskPathData: () => shapeMaskPathData,
|
|
87
|
+
toCanvasSpace: () => toCanvasSpace,
|
|
88
|
+
toHostSpace: () => toHostSpace,
|
|
89
|
+
unwrapGroup: () => unwrapGroup
|
|
82
90
|
});
|
|
83
91
|
module.exports = __toCommonJS(index_exports);
|
|
84
92
|
|
|
85
93
|
// src/editor.ts
|
|
86
|
-
var
|
|
94
|
+
var import_fabric17 = require("fabric");
|
|
87
95
|
|
|
88
96
|
// src/events.ts
|
|
89
97
|
var EventEmitter = class {
|
|
@@ -245,7 +253,13 @@ var LayerManager = class {
|
|
|
245
253
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
246
254
|
layer.renderProxy = proxy;
|
|
247
255
|
if (proxy) {
|
|
248
|
-
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
|
+
});
|
|
249
263
|
this.canvas.add(proxy);
|
|
250
264
|
this.syncZOrder();
|
|
251
265
|
}
|
|
@@ -284,7 +298,7 @@ var LayerManager = class {
|
|
|
284
298
|
} else {
|
|
285
299
|
const layer = this.get(id);
|
|
286
300
|
if (layer) {
|
|
287
|
-
this.canvas.setActiveObject(layer.fabricObject);
|
|
301
|
+
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
304
|
this.canvas.requestRenderAll();
|
|
@@ -319,8 +333,9 @@ var LayerManager = class {
|
|
|
319
333
|
if (!layer) return;
|
|
320
334
|
if (layer.locked === locked) return;
|
|
321
335
|
layer.locked = locked;
|
|
322
|
-
layer.
|
|
323
|
-
|
|
336
|
+
const target = layer.renderProxy ?? layer.fabricObject;
|
|
337
|
+
target.selectable = !locked;
|
|
338
|
+
target.evented = !locked;
|
|
324
339
|
this.canvas.requestRenderAll();
|
|
325
340
|
this.emitChanged();
|
|
326
341
|
this.onPropertyChanged?.();
|
|
@@ -987,41 +1002,37 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
987
1002
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
988
1003
|
source;
|
|
989
1004
|
config;
|
|
1005
|
+
/** The print area this pattern fills, in canvas units. */
|
|
1006
|
+
area;
|
|
990
1007
|
snapshotEl = null;
|
|
991
1008
|
snapshotScale = 0;
|
|
992
|
-
constructor(source, config,
|
|
1009
|
+
constructor(source, config, area) {
|
|
993
1010
|
super({
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
originX: "left",
|
|
998
|
-
originY: "top",
|
|
999
|
-
left: 0,
|
|
1000
|
-
top: 0,
|
|
1001
|
-
width,
|
|
1002
|
-
height,
|
|
1003
|
-
// Pointer events belong to the source underneath, and fabric's object
|
|
1004
|
-
// cache would freeze the tiling at screen resolution.
|
|
1005
|
-
selectable: false,
|
|
1006
|
-
evented: false,
|
|
1011
|
+
// Centre origin: the box tracks a tile, and a tile is placed by its centre.
|
|
1012
|
+
originX: "center",
|
|
1013
|
+
originY: "center",
|
|
1007
1014
|
objectCaching: false,
|
|
1008
|
-
|
|
1009
|
-
|
|
1015
|
+
// Side handles would imply a non-uniform tile scale; the config has one.
|
|
1016
|
+
lockScalingFlip: true
|
|
1010
1017
|
});
|
|
1011
1018
|
this.source = source;
|
|
1012
1019
|
this.config = config;
|
|
1020
|
+
this.area = area;
|
|
1021
|
+
this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
|
|
1022
|
+
this.syncBox();
|
|
1013
1023
|
}
|
|
1014
|
-
/** Swap in a new config
|
|
1024
|
+
/** Swap in a new config and re-fit the box to the tile it now describes. */
|
|
1015
1025
|
setConfig(config) {
|
|
1016
1026
|
this.config = config;
|
|
1017
|
-
this.
|
|
1027
|
+
this.syncBox();
|
|
1018
1028
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
setArea(
|
|
1021
|
-
this.
|
|
1022
|
-
this.
|
|
1029
|
+
/** Re-fit to a new print area (canvas resize). */
|
|
1030
|
+
setArea(area) {
|
|
1031
|
+
this.area = area;
|
|
1032
|
+
this.invalidate();
|
|
1033
|
+
this.syncBox();
|
|
1023
1034
|
}
|
|
1024
|
-
/** Drop the cached source snapshot (
|
|
1035
|
+
/** Drop the cached source snapshot (the source was edited). */
|
|
1025
1036
|
invalidate() {
|
|
1026
1037
|
this.snapshotEl = null;
|
|
1027
1038
|
this.snapshotScale = 0;
|
|
@@ -1032,40 +1043,105 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1032
1043
|
this.snapshotEl = null;
|
|
1033
1044
|
this.snapshotScale = 0;
|
|
1034
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
|
+
}
|
|
1035
1085
|
_render(ctx) {
|
|
1036
|
-
const width = this.
|
|
1037
|
-
const height = this.height ?? 0;
|
|
1086
|
+
const { width, height } = this.area;
|
|
1038
1087
|
if (width <= 0 || height <= 0) return;
|
|
1039
|
-
const
|
|
1040
|
-
const snapshot = this.ensureSnapshot(contextScale(ctx) *
|
|
1088
|
+
const { tileW, tileH } = this.liveTile();
|
|
1089
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
|
|
1041
1090
|
if (!snapshot) return;
|
|
1042
|
-
const
|
|
1091
|
+
const centre = this.getCenterPoint();
|
|
1092
|
+
const angle = this.liveAngle();
|
|
1043
1093
|
ctx.save();
|
|
1044
|
-
ctx.
|
|
1045
|
-
|
|
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);
|
|
1046
1101
|
ctx.restore();
|
|
1047
1102
|
}
|
|
1048
1103
|
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
1049
1104
|
_toSVG() {
|
|
1050
|
-
const width = this.
|
|
1051
|
-
const height = this.height ?? 0;
|
|
1105
|
+
const { width, height } = this.area;
|
|
1052
1106
|
if (width <= 0 || height <= 0) return [];
|
|
1053
1107
|
const el = document.createElement("canvas");
|
|
1054
1108
|
el.width = Math.max(1, Math.round(width));
|
|
1055
1109
|
el.height = Math.max(1, Math.round(height));
|
|
1056
1110
|
const ctx = el.getContext("2d");
|
|
1057
1111
|
if (!ctx) return [];
|
|
1058
|
-
const
|
|
1059
|
-
const snapshot = this.ensureSnapshot(
|
|
1112
|
+
const { tileW, tileH } = this.liveTile();
|
|
1113
|
+
const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
|
|
1060
1114
|
if (!snapshot) return [];
|
|
1061
|
-
const
|
|
1062
|
-
|
|
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
|
+
);
|
|
1063
1128
|
return [
|
|
1064
|
-
`<
|
|
1065
|
-
|
|
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>
|
|
1066
1132
|
`
|
|
1067
1133
|
];
|
|
1068
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
|
+
}
|
|
1069
1145
|
/**
|
|
1070
1146
|
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
1071
1147
|
* registry, which cannot carry a live source reference. Export paths clone
|
|
@@ -1073,46 +1149,64 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1073
1149
|
* tiling. The copy shares the source (it only ever reads from it).
|
|
1074
1150
|
*/
|
|
1075
1151
|
clone() {
|
|
1076
|
-
const copy = new _TiledPatternObject(
|
|
1077
|
-
this.source,
|
|
1078
|
-
this.config,
|
|
1079
|
-
this.width ?? 0,
|
|
1080
|
-
this.height ?? 0
|
|
1081
|
-
);
|
|
1152
|
+
const copy = new _TiledPatternObject(this.source, this.config, this.area);
|
|
1082
1153
|
copy.set({
|
|
1083
1154
|
left: this.left,
|
|
1084
1155
|
top: this.top,
|
|
1156
|
+
angle: this.angle,
|
|
1157
|
+
scaleX: this.scaleX,
|
|
1158
|
+
scaleY: this.scaleY,
|
|
1159
|
+
width: this.width,
|
|
1160
|
+
height: this.height,
|
|
1085
1161
|
visible: this.visible,
|
|
1086
1162
|
opacity: this.opacity
|
|
1087
1163
|
});
|
|
1088
1164
|
return Promise.resolve(copy);
|
|
1089
1165
|
}
|
|
1090
|
-
/**
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1094
|
-
*/
|
|
1095
|
-
toObject() {
|
|
1096
|
-
const plain = super.toObject();
|
|
1097
|
-
delete plain.source;
|
|
1098
|
-
return plain;
|
|
1166
|
+
/** The source's on-canvas width, before the tile scale. */
|
|
1167
|
+
baseW() {
|
|
1168
|
+
return Math.max(1, this.source.getBoundingRect().width);
|
|
1099
1169
|
}
|
|
1100
|
-
/** Tile size
|
|
1101
|
-
|
|
1102
|
-
const
|
|
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();
|
|
1103
1175
|
return {
|
|
1104
|
-
tileW: Math.max(floor,
|
|
1105
|
-
tileH: Math.max(floor,
|
|
1176
|
+
tileW: Math.max(floor, rect.width * scale),
|
|
1177
|
+
tileH: Math.max(floor, rect.height * scale)
|
|
1106
1178
|
};
|
|
1107
1179
|
}
|
|
1108
|
-
/**
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
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);
|
|
1112
1206
|
}
|
|
1113
1207
|
/**
|
|
1114
|
-
* Snapshot the source at (at least) `scale`, reusing the cached one
|
|
1115
|
-
* still sharp enough and the source has not changed.
|
|
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.
|
|
1116
1210
|
*/
|
|
1117
1211
|
ensureSnapshot(scale) {
|
|
1118
1212
|
const wanted = this.clampScale(scale);
|
|
@@ -1143,6 +1237,10 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1143
1237
|
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1144
1238
|
}
|
|
1145
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
|
+
}
|
|
1146
1244
|
function contextScale(ctx) {
|
|
1147
1245
|
if (typeof ctx.getTransform !== "function") return 1;
|
|
1148
1246
|
try {
|
|
@@ -1154,13 +1252,15 @@ function contextScale(ctx) {
|
|
|
1154
1252
|
}
|
|
1155
1253
|
|
|
1156
1254
|
// src/pattern/pattern-manager.ts
|
|
1255
|
+
var MIN_TILE_SCALE = 10;
|
|
1256
|
+
var MAX_TILE_SCALE = 300;
|
|
1157
1257
|
var PatternManager = class {
|
|
1158
1258
|
constructor(canvas, layers, history, events) {
|
|
1159
1259
|
this.canvas = canvas;
|
|
1160
1260
|
this.layers = layers;
|
|
1161
1261
|
this.history = history;
|
|
1162
1262
|
this.events = events;
|
|
1163
|
-
this.canvas.on("object:modified", this.
|
|
1263
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
1164
1264
|
this.canvas.on("text:changed", this.onSourceModified);
|
|
1165
1265
|
this.events.on("layer:removed", this.onLayerRemoved);
|
|
1166
1266
|
}
|
|
@@ -1169,6 +1269,11 @@ var PatternManager = class {
|
|
|
1169
1269
|
history;
|
|
1170
1270
|
events;
|
|
1171
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
|
+
};
|
|
1172
1277
|
onSourceModified = (event) => {
|
|
1173
1278
|
this.invalidateFor(event.target);
|
|
1174
1279
|
};
|
|
@@ -1210,7 +1315,11 @@ var PatternManager = class {
|
|
|
1210
1315
|
try {
|
|
1211
1316
|
this.detach(layerId);
|
|
1212
1317
|
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1213
|
-
layer.fabricObject.set({
|
|
1318
|
+
layer.fabricObject.set({
|
|
1319
|
+
opacity: layer.opacity,
|
|
1320
|
+
selectable: !layer.locked,
|
|
1321
|
+
evented: !layer.locked
|
|
1322
|
+
});
|
|
1214
1323
|
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
1215
1324
|
this.canvas.requestRenderAll();
|
|
1216
1325
|
this.history.save();
|
|
@@ -1242,12 +1351,8 @@ var PatternManager = class {
|
|
|
1242
1351
|
}
|
|
1243
1352
|
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1244
1353
|
syncArea() {
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
for (const proxy of this.proxies.values()) {
|
|
1248
|
-
proxy.setArea(width, height);
|
|
1249
|
-
proxy.invalidate();
|
|
1250
|
-
}
|
|
1354
|
+
const area = this.area();
|
|
1355
|
+
for (const proxy of this.proxies.values()) proxy.setArea(area);
|
|
1251
1356
|
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1252
1357
|
}
|
|
1253
1358
|
/** Drop a layer's cached source snapshot (its content changed). */
|
|
@@ -1255,6 +1360,7 @@ var PatternManager = class {
|
|
|
1255
1360
|
const proxy = this.proxies.get(layerId);
|
|
1256
1361
|
if (!proxy) return;
|
|
1257
1362
|
proxy.invalidate();
|
|
1363
|
+
proxy.syncBox();
|
|
1258
1364
|
this.canvas.requestRenderAll();
|
|
1259
1365
|
}
|
|
1260
1366
|
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
@@ -1264,7 +1370,7 @@ var PatternManager = class {
|
|
|
1264
1370
|
this.attach(layer, state.config);
|
|
1265
1371
|
}
|
|
1266
1372
|
dispose() {
|
|
1267
|
-
this.canvas.off("object:modified", this.
|
|
1373
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
1268
1374
|
this.canvas.off("text:changed", this.onSourceModified);
|
|
1269
1375
|
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1270
1376
|
this.clearProxies();
|
|
@@ -1275,17 +1381,38 @@ var PatternManager = class {
|
|
|
1275
1381
|
this.proxies.clear();
|
|
1276
1382
|
}
|
|
1277
1383
|
attach(layer, config) {
|
|
1278
|
-
const proxy = new TiledPatternObject(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
this.canvas.getWidth(),
|
|
1282
|
-
this.canvas.getHeight()
|
|
1283
|
-
);
|
|
1284
|
-
layer.fabricObject.set({ opacity: 0 });
|
|
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 });
|
|
1285
1387
|
this.proxies.set(layer.id, proxy);
|
|
1286
1388
|
this.layers.setRenderProxy(layer.id, proxy);
|
|
1389
|
+
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1287
1390
|
this.canvas.requestRenderAll();
|
|
1288
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);
|
|
1410
|
+
this.canvas.requestRenderAll();
|
|
1411
|
+
this.history.save();
|
|
1412
|
+
}
|
|
1413
|
+
area() {
|
|
1414
|
+
return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
|
|
1415
|
+
}
|
|
1289
1416
|
detach(layerId) {
|
|
1290
1417
|
const proxy = this.proxies.get(layerId);
|
|
1291
1418
|
if (!proxy) {
|
|
@@ -1302,6 +1429,15 @@ var PatternManager = class {
|
|
|
1302
1429
|
if (layer) this.invalidate(layer.id);
|
|
1303
1430
|
}
|
|
1304
1431
|
};
|
|
1432
|
+
function clamp2(value, min, max) {
|
|
1433
|
+
if (!Number.isFinite(value)) return min;
|
|
1434
|
+
return Math.max(min, Math.min(max, value));
|
|
1435
|
+
}
|
|
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
|
+
}
|
|
1305
1441
|
function isLegacyState(state) {
|
|
1306
1442
|
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1307
1443
|
}
|
|
@@ -2852,6 +2988,737 @@ var MaskController = class {
|
|
|
2852
2988
|
}
|
|
2853
2989
|
};
|
|
2854
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
|
+
|
|
2855
3722
|
// src/editor.ts
|
|
2856
3723
|
var MIN_ZOOM = 0.1;
|
|
2857
3724
|
var MAX_ZOOM = 8;
|
|
@@ -2869,6 +3736,8 @@ var CanvasEditor = class {
|
|
|
2869
3736
|
patterns;
|
|
2870
3737
|
curves;
|
|
2871
3738
|
maskPresets;
|
|
3739
|
+
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
3740
|
+
layerMasks;
|
|
2872
3741
|
fonts;
|
|
2873
3742
|
licensing;
|
|
2874
3743
|
pages;
|
|
@@ -2893,7 +3762,7 @@ var CanvasEditor = class {
|
|
|
2893
3762
|
const widthPx = this.units.toPixels(config.width);
|
|
2894
3763
|
const heightPx = this.units.toPixels(config.height);
|
|
2895
3764
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2896
|
-
this.canvas = new
|
|
3765
|
+
this.canvas = new import_fabric17.Canvas(canvasElement, {
|
|
2897
3766
|
width: widthPx,
|
|
2898
3767
|
height: heightPx,
|
|
2899
3768
|
backgroundColor: this.designBackground,
|
|
@@ -2917,6 +3786,7 @@ var CanvasEditor = class {
|
|
|
2917
3786
|
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2918
3787
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2919
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);
|
|
2920
3790
|
this.setupCanvasEvents();
|
|
2921
3791
|
this.refreshSelectionStyle();
|
|
2922
3792
|
this.history.saveImmediate();
|
|
@@ -2926,12 +3796,13 @@ var CanvasEditor = class {
|
|
|
2926
3796
|
// ─── Layer Operations ────────────────────────────────
|
|
2927
3797
|
async addImage(url, options) {
|
|
2928
3798
|
try {
|
|
2929
|
-
const img = await
|
|
3799
|
+
const img = await import_fabric17.FabricImage.fromURL(
|
|
2930
3800
|
url,
|
|
2931
3801
|
{},
|
|
2932
3802
|
{ originX: "left", originY: "top", ...options }
|
|
2933
3803
|
);
|
|
2934
3804
|
const layer = this.layers.add("image", img);
|
|
3805
|
+
this.layers.select(layer.id);
|
|
2935
3806
|
this.history.save();
|
|
2936
3807
|
return layer;
|
|
2937
3808
|
} catch (error) {
|
|
@@ -2951,7 +3822,7 @@ var CanvasEditor = class {
|
|
|
2951
3822
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2952
3823
|
const previous = layer.fabricObject;
|
|
2953
3824
|
try {
|
|
2954
|
-
const replacement = await
|
|
3825
|
+
const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2955
3826
|
replacement.set({
|
|
2956
3827
|
left: previous.left,
|
|
2957
3828
|
top: previous.top,
|
|
@@ -2980,7 +3851,7 @@ var CanvasEditor = class {
|
|
|
2980
3851
|
}
|
|
2981
3852
|
}
|
|
2982
3853
|
addText(text, options) {
|
|
2983
|
-
const textbox = new
|
|
3854
|
+
const textbox = new import_fabric17.Textbox(text, {
|
|
2984
3855
|
fontSize: 32,
|
|
2985
3856
|
fontFamily: "Arial",
|
|
2986
3857
|
fill: "#000000",
|
|
@@ -2991,12 +3862,14 @@ var CanvasEditor = class {
|
|
|
2991
3862
|
...options
|
|
2992
3863
|
});
|
|
2993
3864
|
const layer = this.layers.add("text", textbox);
|
|
3865
|
+
this.layers.select(layer.id);
|
|
2994
3866
|
this.history.save();
|
|
2995
3867
|
return layer;
|
|
2996
3868
|
}
|
|
2997
3869
|
addShape(plugin, options) {
|
|
2998
3870
|
const obj = plugin.create(options);
|
|
2999
3871
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
3872
|
+
this.layers.select(layer.id);
|
|
3000
3873
|
this.history.save();
|
|
3001
3874
|
return layer;
|
|
3002
3875
|
}
|
|
@@ -3019,10 +3892,10 @@ var CanvasEditor = class {
|
|
|
3019
3892
|
return value === void 0 ? token : escapeXml(value);
|
|
3020
3893
|
})
|
|
3021
3894
|
);
|
|
3022
|
-
const { objects, options } = await (0,
|
|
3895
|
+
const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
|
|
3023
3896
|
const validObjects = objects.filter((object) => object !== null);
|
|
3024
3897
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
3025
|
-
const group =
|
|
3898
|
+
const group = import_fabric17.util.groupSVGElements(validObjects, options);
|
|
3026
3899
|
group.set({
|
|
3027
3900
|
left: this.canvas.getWidth() / 2,
|
|
3028
3901
|
top: this.canvas.getHeight() / 2,
|
|
@@ -3030,6 +3903,7 @@ var CanvasEditor = class {
|
|
|
3030
3903
|
originY: "center"
|
|
3031
3904
|
});
|
|
3032
3905
|
const layer = this.layers.add("template", group, template.name);
|
|
3906
|
+
this.layers.select(layer.id);
|
|
3033
3907
|
this.history.save();
|
|
3034
3908
|
return layer;
|
|
3035
3909
|
}
|
|
@@ -3081,7 +3955,7 @@ var CanvasEditor = class {
|
|
|
3081
3955
|
opacity: layer.opacity
|
|
3082
3956
|
});
|
|
3083
3957
|
this.patterns.attachTo(copy);
|
|
3084
|
-
this.
|
|
3958
|
+
this.layers.select(copy.id);
|
|
3085
3959
|
this.canvas.requestRenderAll();
|
|
3086
3960
|
this.history.save();
|
|
3087
3961
|
return copy;
|
|
@@ -3094,10 +3968,10 @@ var CanvasEditor = class {
|
|
|
3094
3968
|
const next = { ...previous, ...adjustments };
|
|
3095
3969
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
3096
3970
|
image.filters = [
|
|
3097
|
-
new
|
|
3098
|
-
new
|
|
3099
|
-
new
|
|
3100
|
-
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) })
|
|
3101
3975
|
];
|
|
3102
3976
|
layer.meta.imageAdjustments = next;
|
|
3103
3977
|
image.applyFilters();
|
|
@@ -3114,7 +3988,7 @@ var CanvasEditor = class {
|
|
|
3114
3988
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
3115
3989
|
const objects = children.map((layer) => layer.fabricObject);
|
|
3116
3990
|
for (const layer of children) this.layers.remove(layer.id);
|
|
3117
|
-
const group = new
|
|
3991
|
+
const group = new import_fabric17.Group(objects);
|
|
3118
3992
|
const grouped = this.layers.add("group", group, name);
|
|
3119
3993
|
grouped.meta.groupChildren = childData;
|
|
3120
3994
|
this.layers.select(grouped.id);
|
|
@@ -3129,11 +4003,9 @@ var CanvasEditor = class {
|
|
|
3129
4003
|
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
3130
4004
|
const group = grouped.fabricObject;
|
|
3131
4005
|
return this.history.transaction(() => {
|
|
3132
|
-
const transform = group.calcTransformMatrix();
|
|
3133
4006
|
const objects = group.removeAll();
|
|
3134
4007
|
this.layers.remove(id);
|
|
3135
4008
|
const restored = objects.map((object, index) => {
|
|
3136
|
-
import_fabric10.util.addTransformToObject(object, transform);
|
|
3137
4009
|
object.setCoords();
|
|
3138
4010
|
const data = childData[index];
|
|
3139
4011
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -3161,6 +4033,7 @@ var CanvasEditor = class {
|
|
|
3161
4033
|
try {
|
|
3162
4034
|
await deserializeEditor(this, state);
|
|
3163
4035
|
await this.patterns.rehydrateAll();
|
|
4036
|
+
this.layerMasks.refreshAll();
|
|
3164
4037
|
} catch (error) {
|
|
3165
4038
|
if (!managedByHistory) {
|
|
3166
4039
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3189,7 +4062,7 @@ var CanvasEditor = class {
|
|
|
3189
4062
|
const layer = this.layers.get(id);
|
|
3190
4063
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
3191
4064
|
try {
|
|
3192
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4065
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
|
|
3193
4066
|
const image = await layer.fabricObject.clone();
|
|
3194
4067
|
image.set({
|
|
3195
4068
|
left: 0,
|
|
@@ -3465,7 +4338,7 @@ var CanvasEditor = class {
|
|
|
3465
4338
|
return;
|
|
3466
4339
|
}
|
|
3467
4340
|
try {
|
|
3468
|
-
const image = await
|
|
4341
|
+
const image = await import_fabric17.FabricImage.fromURL(
|
|
3469
4342
|
url,
|
|
3470
4343
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3471
4344
|
{ originX: "left", originY: "top" }
|
|
@@ -3733,6 +4606,7 @@ var CanvasEditor = class {
|
|
|
3733
4606
|
// ─── Cleanup ────────────────────────────────────────
|
|
3734
4607
|
dispose() {
|
|
3735
4608
|
this.masks.dispose();
|
|
4609
|
+
this.layerMasks.dispose();
|
|
3736
4610
|
this.snapping.dispose();
|
|
3737
4611
|
this.crop.dispose();
|
|
3738
4612
|
this.history.dispose();
|
|
@@ -3856,6 +4730,7 @@ var AnnotationOverlay = class {
|
|
|
3856
4730
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3857
4731
|
0 && (module.exports = {
|
|
3858
4732
|
AnnotationOverlay,
|
|
4733
|
+
CANVAS_MASK_TARGET,
|
|
3859
4734
|
CANVAS_SIZE_PRESETS,
|
|
3860
4735
|
CanvasEditor,
|
|
3861
4736
|
CropController,
|
|
@@ -3868,6 +4743,7 @@ var AnnotationOverlay = class {
|
|
|
3868
4743
|
HistoryManager,
|
|
3869
4744
|
Layer,
|
|
3870
4745
|
LayerManager,
|
|
4746
|
+
LayerMaskManager,
|
|
3871
4747
|
LicenseManager,
|
|
3872
4748
|
MaskController,
|
|
3873
4749
|
MaskPresetManager,
|
|
@@ -3889,6 +4765,7 @@ var AnnotationOverlay = class {
|
|
|
3889
4765
|
buildCurvePathData,
|
|
3890
4766
|
clamp,
|
|
3891
4767
|
clearTextureMaskCache,
|
|
4768
|
+
composeMaskGroup,
|
|
3892
4769
|
computeCoverPlacement,
|
|
3893
4770
|
computePrintAreaClip,
|
|
3894
4771
|
computeTilePositions,
|
|
@@ -3901,11 +4778,13 @@ var AnnotationOverlay = class {
|
|
|
3901
4778
|
exportPNG,
|
|
3902
4779
|
exportPrintArea,
|
|
3903
4780
|
exportSVG,
|
|
4781
|
+
fitToBox,
|
|
3904
4782
|
generateId,
|
|
3905
4783
|
isCssColor,
|
|
3906
4784
|
isMaskPresetId,
|
|
3907
4785
|
isShapeMaskId,
|
|
3908
4786
|
isTextureMaskId,
|
|
4787
|
+
needsAbsoluteSpace,
|
|
3909
4788
|
readLayerShadow,
|
|
3910
4789
|
renderTextureMask,
|
|
3911
4790
|
resetTransform,
|
|
@@ -3913,6 +4792,9 @@ var AnnotationOverlay = class {
|
|
|
3913
4792
|
round2,
|
|
3914
4793
|
sanitizeSvg,
|
|
3915
4794
|
serializeEditor,
|
|
3916
|
-
shapeMaskPathData
|
|
4795
|
+
shapeMaskPathData,
|
|
4796
|
+
toCanvasSpace,
|
|
4797
|
+
toHostSpace,
|
|
4798
|
+
unwrapGroup
|
|
3917
4799
|
});
|
|
3918
4800
|
//# sourceMappingURL=index.js.map
|