@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.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./chunk-MCBRZQ4M.mjs";
|
|
12
12
|
|
|
13
13
|
// src/editor.ts
|
|
14
|
-
import { Canvas, FabricImage as FabricImage3, Group, Textbox, filters, loadSVGFromString, util as
|
|
14
|
+
import { Canvas, FabricImage as FabricImage3, Group as Group6, Textbox, filters, loadSVGFromString, util as util5 } from "fabric";
|
|
15
15
|
|
|
16
16
|
// src/events.ts
|
|
17
17
|
var EventEmitter = class {
|
|
@@ -173,7 +173,13 @@ var LayerManager = class {
|
|
|
173
173
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
174
174
|
layer.renderProxy = proxy;
|
|
175
175
|
if (proxy) {
|
|
176
|
-
proxy.
|
|
176
|
+
proxy._layerId = id;
|
|
177
|
+
proxy.set({
|
|
178
|
+
visible: layer.visible,
|
|
179
|
+
opacity: layer.opacity,
|
|
180
|
+
selectable: !layer.locked,
|
|
181
|
+
evented: !layer.locked
|
|
182
|
+
});
|
|
177
183
|
this.canvas.add(proxy);
|
|
178
184
|
this.syncZOrder();
|
|
179
185
|
}
|
|
@@ -212,7 +218,7 @@ var LayerManager = class {
|
|
|
212
218
|
} else {
|
|
213
219
|
const layer = this.get(id);
|
|
214
220
|
if (layer) {
|
|
215
|
-
this.canvas.setActiveObject(layer.fabricObject);
|
|
221
|
+
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
this.canvas.requestRenderAll();
|
|
@@ -247,8 +253,9 @@ var LayerManager = class {
|
|
|
247
253
|
if (!layer) return;
|
|
248
254
|
if (layer.locked === locked) return;
|
|
249
255
|
layer.locked = locked;
|
|
250
|
-
layer.
|
|
251
|
-
|
|
256
|
+
const target = layer.renderProxy ?? layer.fabricObject;
|
|
257
|
+
target.selectable = !locked;
|
|
258
|
+
target.evented = !locked;
|
|
252
259
|
this.canvas.requestRenderAll();
|
|
253
260
|
this.emitChanged();
|
|
254
261
|
this.onPropertyChanged?.();
|
|
@@ -846,7 +853,7 @@ var STROKE2 = "#22c55e";
|
|
|
846
853
|
import { util } from "fabric";
|
|
847
854
|
|
|
848
855
|
// src/pattern/tiled-pattern-object.ts
|
|
849
|
-
import { FabricObject } from "fabric";
|
|
856
|
+
import { FabricObject, Point } from "fabric";
|
|
850
857
|
|
|
851
858
|
// src/pattern/tile-geometry.ts
|
|
852
859
|
var MAX_TILES_PER_AXIS = 200;
|
|
@@ -915,41 +922,37 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
915
922
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
916
923
|
source;
|
|
917
924
|
config;
|
|
925
|
+
/** The print area this pattern fills, in canvas units. */
|
|
926
|
+
area;
|
|
918
927
|
snapshotEl = null;
|
|
919
928
|
snapshotScale = 0;
|
|
920
|
-
constructor(source, config,
|
|
929
|
+
constructor(source, config, area) {
|
|
921
930
|
super({
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
originX: "left",
|
|
926
|
-
originY: "top",
|
|
927
|
-
left: 0,
|
|
928
|
-
top: 0,
|
|
929
|
-
width,
|
|
930
|
-
height,
|
|
931
|
-
// Pointer events belong to the source underneath, and fabric's object
|
|
932
|
-
// cache would freeze the tiling at screen resolution.
|
|
933
|
-
selectable: false,
|
|
934
|
-
evented: false,
|
|
931
|
+
// Centre origin: the box tracks a tile, and a tile is placed by its centre.
|
|
932
|
+
originX: "center",
|
|
933
|
+
originY: "center",
|
|
935
934
|
objectCaching: false,
|
|
936
|
-
|
|
937
|
-
|
|
935
|
+
// Side handles would imply a non-uniform tile scale; the config has one.
|
|
936
|
+
lockScalingFlip: true
|
|
938
937
|
});
|
|
939
938
|
this.source = source;
|
|
940
939
|
this.config = config;
|
|
940
|
+
this.area = area;
|
|
941
|
+
this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
|
|
942
|
+
this.syncBox();
|
|
941
943
|
}
|
|
942
|
-
/** Swap in a new config
|
|
944
|
+
/** Swap in a new config and re-fit the box to the tile it now describes. */
|
|
943
945
|
setConfig(config) {
|
|
944
946
|
this.config = config;
|
|
945
|
-
this.
|
|
947
|
+
this.syncBox();
|
|
946
948
|
}
|
|
947
|
-
/**
|
|
948
|
-
setArea(
|
|
949
|
-
this.
|
|
950
|
-
this.
|
|
949
|
+
/** Re-fit to a new print area (canvas resize). */
|
|
950
|
+
setArea(area) {
|
|
951
|
+
this.area = area;
|
|
952
|
+
this.invalidate();
|
|
953
|
+
this.syncBox();
|
|
951
954
|
}
|
|
952
|
-
/** Drop the cached source snapshot (
|
|
955
|
+
/** Drop the cached source snapshot (the source was edited). */
|
|
953
956
|
invalidate() {
|
|
954
957
|
this.snapshotEl = null;
|
|
955
958
|
this.snapshotScale = 0;
|
|
@@ -960,40 +963,105 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
960
963
|
this.snapshotEl = null;
|
|
961
964
|
this.snapshotScale = 0;
|
|
962
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Put the box back on the anchor tile: the tile's size, the pattern angle, and
|
|
968
|
+
* the grid origin displaced by the configured phase shift. Resets any live
|
|
969
|
+
* gesture scale, so it must run only once that gesture has been folded in.
|
|
970
|
+
*/
|
|
971
|
+
syncBox() {
|
|
972
|
+
const { tileW, tileH } = this.baseTile();
|
|
973
|
+
const origin = this.source.getCenterPoint();
|
|
974
|
+
const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
|
|
975
|
+
this.set({
|
|
976
|
+
left: anchor.x,
|
|
977
|
+
top: anchor.y,
|
|
978
|
+
width: tileW,
|
|
979
|
+
height: tileH,
|
|
980
|
+
scaleX: 1,
|
|
981
|
+
scaleY: 1,
|
|
982
|
+
angle: this.config.angle
|
|
983
|
+
});
|
|
984
|
+
this.setCoords();
|
|
985
|
+
this.dirty = true;
|
|
986
|
+
}
|
|
987
|
+
/** Grid rotation in play right now — the live handle during a rotate gesture. */
|
|
988
|
+
liveAngle() {
|
|
989
|
+
return this.angle ?? 0;
|
|
990
|
+
}
|
|
991
|
+
/** Tile scale in play right now, as a config percentage. */
|
|
992
|
+
liveScale() {
|
|
993
|
+
return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Where the source's centre would have to sit for the box to stay put — i.e.
|
|
997
|
+
* the grid origin implied by a drag. `PatternManager` moves the source there.
|
|
998
|
+
*/
|
|
999
|
+
liveOrigin() {
|
|
1000
|
+
const { tileW, tileH } = this.liveTile();
|
|
1001
|
+
const centre = this.getCenterPoint();
|
|
1002
|
+
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1003
|
+
return new Point(centre.x - shift.x, centre.y - shift.y);
|
|
1004
|
+
}
|
|
963
1005
|
_render(ctx) {
|
|
964
|
-
const width = this.
|
|
965
|
-
const height = this.height ?? 0;
|
|
1006
|
+
const { width, height } = this.area;
|
|
966
1007
|
if (width <= 0 || height <= 0) return;
|
|
967
|
-
const
|
|
968
|
-
const snapshot = this.ensureSnapshot(contextScale(ctx) *
|
|
1008
|
+
const { tileW, tileH } = this.liveTile();
|
|
1009
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
|
|
969
1010
|
if (!snapshot) return;
|
|
970
|
-
const
|
|
1011
|
+
const centre = this.getCenterPoint();
|
|
1012
|
+
const angle = this.liveAngle();
|
|
971
1013
|
ctx.save();
|
|
972
|
-
ctx.
|
|
973
|
-
|
|
1014
|
+
ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
|
|
1015
|
+
ctx.rotate(-angle * Math.PI / 180);
|
|
1016
|
+
ctx.translate(-centre.x, -centre.y);
|
|
1017
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1018
|
+
const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
|
|
1019
|
+
const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
|
|
1020
|
+
drawTiles(ctx, snapshot, config, width, height, tileW, tileH, origin);
|
|
974
1021
|
ctx.restore();
|
|
975
1022
|
}
|
|
976
1023
|
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
977
1024
|
_toSVG() {
|
|
978
|
-
const width = this.
|
|
979
|
-
const height = this.height ?? 0;
|
|
1025
|
+
const { width, height } = this.area;
|
|
980
1026
|
if (width <= 0 || height <= 0) return [];
|
|
981
1027
|
const el = document.createElement("canvas");
|
|
982
1028
|
el.width = Math.max(1, Math.round(width));
|
|
983
1029
|
el.height = Math.max(1, Math.round(height));
|
|
984
1030
|
const ctx = el.getContext("2d");
|
|
985
1031
|
if (!ctx) return [];
|
|
986
|
-
const
|
|
987
|
-
const snapshot = this.ensureSnapshot(
|
|
1032
|
+
const { tileW, tileH } = this.liveTile();
|
|
1033
|
+
const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
|
|
988
1034
|
if (!snapshot) return [];
|
|
989
|
-
const
|
|
990
|
-
|
|
1035
|
+
const centre = this.getCenterPoint();
|
|
1036
|
+
const angle = this.liveAngle();
|
|
1037
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1038
|
+
drawTiles(
|
|
1039
|
+
ctx,
|
|
1040
|
+
snapshot,
|
|
1041
|
+
{ ...this.config, angle, offsetX: 0, offsetY: 0 },
|
|
1042
|
+
width,
|
|
1043
|
+
height,
|
|
1044
|
+
tileW,
|
|
1045
|
+
tileH,
|
|
1046
|
+
{ x: centre.x - shift.x, y: centre.y - shift.y }
|
|
1047
|
+
);
|
|
991
1048
|
return [
|
|
992
|
-
`<
|
|
993
|
-
|
|
1049
|
+
`<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
|
|
1050
|
+
`<image x="0" y="0" width="${width}" height="${height}" `,
|
|
1051
|
+
`xlink:href="${el.toDataURL("image/png")}"></image></g>
|
|
994
1052
|
`
|
|
995
1053
|
];
|
|
996
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1057
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1058
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1059
|
+
*/
|
|
1060
|
+
toObject() {
|
|
1061
|
+
const plain = super.toObject();
|
|
1062
|
+
delete plain.source;
|
|
1063
|
+
return plain;
|
|
1064
|
+
}
|
|
997
1065
|
/**
|
|
998
1066
|
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
999
1067
|
* registry, which cannot carry a live source reference. Export paths clone
|
|
@@ -1001,46 +1069,64 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1001
1069
|
* tiling. The copy shares the source (it only ever reads from it).
|
|
1002
1070
|
*/
|
|
1003
1071
|
clone() {
|
|
1004
|
-
const copy = new _TiledPatternObject(
|
|
1005
|
-
this.source,
|
|
1006
|
-
this.config,
|
|
1007
|
-
this.width ?? 0,
|
|
1008
|
-
this.height ?? 0
|
|
1009
|
-
);
|
|
1072
|
+
const copy = new _TiledPatternObject(this.source, this.config, this.area);
|
|
1010
1073
|
copy.set({
|
|
1011
1074
|
left: this.left,
|
|
1012
1075
|
top: this.top,
|
|
1076
|
+
angle: this.angle,
|
|
1077
|
+
scaleX: this.scaleX,
|
|
1078
|
+
scaleY: this.scaleY,
|
|
1079
|
+
width: this.width,
|
|
1080
|
+
height: this.height,
|
|
1013
1081
|
visible: this.visible,
|
|
1014
1082
|
opacity: this.opacity
|
|
1015
1083
|
});
|
|
1016
1084
|
return Promise.resolve(copy);
|
|
1017
1085
|
}
|
|
1018
|
-
/**
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1086
|
+
/** The source's on-canvas width, before the tile scale. */
|
|
1087
|
+
baseW() {
|
|
1088
|
+
return Math.max(1, this.source.getBoundingRect().width);
|
|
1089
|
+
}
|
|
1090
|
+
/** Tile size from the config alone, ignoring any in-flight gesture. */
|
|
1091
|
+
baseTile() {
|
|
1092
|
+
const rect = this.source.getBoundingRect();
|
|
1093
|
+
const scale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1094
|
+
const floor = this.tileFloor();
|
|
1095
|
+
return {
|
|
1096
|
+
tileW: Math.max(floor, rect.width * scale),
|
|
1097
|
+
tileH: Math.max(floor, rect.height * scale)
|
|
1098
|
+
};
|
|
1027
1099
|
}
|
|
1028
|
-
/** Tile size
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1100
|
+
/** Tile size as drawn right now, including a live scale gesture. */
|
|
1101
|
+
liveTile() {
|
|
1102
|
+
const base = this.baseTile();
|
|
1103
|
+
const floor = this.tileFloor();
|
|
1031
1104
|
return {
|
|
1032
|
-
tileW: Math.max(floor,
|
|
1033
|
-
tileH: Math.max(floor,
|
|
1105
|
+
tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
|
|
1106
|
+
tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
|
|
1034
1107
|
};
|
|
1035
1108
|
}
|
|
1036
|
-
/**
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
return
|
|
1109
|
+
/** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
|
|
1110
|
+
tileFloor() {
|
|
1111
|
+
const { width, height } = this.area;
|
|
1112
|
+
return Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1113
|
+
}
|
|
1114
|
+
/** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
|
|
1115
|
+
shiftVector(tileW, tileH, angle) {
|
|
1116
|
+
const dx = tileW * (clampPercent(this.config.offsetX) / 100);
|
|
1117
|
+
const dy = tileH * (clampPercent(this.config.offsetY) / 100);
|
|
1118
|
+
const radians = angle * Math.PI / 180;
|
|
1119
|
+
const cos = Math.cos(radians);
|
|
1120
|
+
const sin = Math.sin(radians);
|
|
1121
|
+
return new Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1122
|
+
}
|
|
1123
|
+
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1124
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1125
|
+
return new Point(origin.x + shift.x, origin.y + shift.y);
|
|
1040
1126
|
}
|
|
1041
1127
|
/**
|
|
1042
|
-
* Snapshot the source at (at least) `scale`, reusing the cached one
|
|
1043
|
-
* still sharp enough and the source has not changed.
|
|
1128
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
1129
|
+
* is still sharp enough and the source has not changed.
|
|
1044
1130
|
*/
|
|
1045
1131
|
ensureSnapshot(scale) {
|
|
1046
1132
|
const wanted = this.clampScale(scale);
|
|
@@ -1071,6 +1157,10 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1071
1157
|
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1072
1158
|
}
|
|
1073
1159
|
};
|
|
1160
|
+
function clampPercent(value) {
|
|
1161
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1162
|
+
return Math.max(-100, Math.min(100, value));
|
|
1163
|
+
}
|
|
1074
1164
|
function contextScale(ctx) {
|
|
1075
1165
|
if (typeof ctx.getTransform !== "function") return 1;
|
|
1076
1166
|
try {
|
|
@@ -1082,13 +1172,15 @@ function contextScale(ctx) {
|
|
|
1082
1172
|
}
|
|
1083
1173
|
|
|
1084
1174
|
// src/pattern/pattern-manager.ts
|
|
1175
|
+
var MIN_TILE_SCALE = 10;
|
|
1176
|
+
var MAX_TILE_SCALE = 300;
|
|
1085
1177
|
var PatternManager = class {
|
|
1086
1178
|
constructor(canvas, layers, history, events) {
|
|
1087
1179
|
this.canvas = canvas;
|
|
1088
1180
|
this.layers = layers;
|
|
1089
1181
|
this.history = history;
|
|
1090
1182
|
this.events = events;
|
|
1091
|
-
this.canvas.on("object:modified", this.
|
|
1183
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
1092
1184
|
this.canvas.on("text:changed", this.onSourceModified);
|
|
1093
1185
|
this.events.on("layer:removed", this.onLayerRemoved);
|
|
1094
1186
|
}
|
|
@@ -1097,6 +1189,11 @@ var PatternManager = class {
|
|
|
1097
1189
|
history;
|
|
1098
1190
|
events;
|
|
1099
1191
|
proxies = /* @__PURE__ */ new Map();
|
|
1192
|
+
onObjectModified = (event) => {
|
|
1193
|
+
const target = event.target;
|
|
1194
|
+
if (target instanceof TiledPatternObject) this.commitGesture(target);
|
|
1195
|
+
else this.invalidateFor(target);
|
|
1196
|
+
};
|
|
1100
1197
|
onSourceModified = (event) => {
|
|
1101
1198
|
this.invalidateFor(event.target);
|
|
1102
1199
|
};
|
|
@@ -1138,7 +1235,11 @@ var PatternManager = class {
|
|
|
1138
1235
|
try {
|
|
1139
1236
|
this.detach(layerId);
|
|
1140
1237
|
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1141
|
-
layer.fabricObject.set({
|
|
1238
|
+
layer.fabricObject.set({
|
|
1239
|
+
opacity: layer.opacity,
|
|
1240
|
+
selectable: !layer.locked,
|
|
1241
|
+
evented: !layer.locked
|
|
1242
|
+
});
|
|
1142
1243
|
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
1143
1244
|
this.canvas.requestRenderAll();
|
|
1144
1245
|
this.history.save();
|
|
@@ -1170,12 +1271,8 @@ var PatternManager = class {
|
|
|
1170
1271
|
}
|
|
1171
1272
|
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1172
1273
|
syncArea() {
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1175
|
-
for (const proxy of this.proxies.values()) {
|
|
1176
|
-
proxy.setArea(width, height);
|
|
1177
|
-
proxy.invalidate();
|
|
1178
|
-
}
|
|
1274
|
+
const area = this.area();
|
|
1275
|
+
for (const proxy of this.proxies.values()) proxy.setArea(area);
|
|
1179
1276
|
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1180
1277
|
}
|
|
1181
1278
|
/** Drop a layer's cached source snapshot (its content changed). */
|
|
@@ -1183,6 +1280,7 @@ var PatternManager = class {
|
|
|
1183
1280
|
const proxy = this.proxies.get(layerId);
|
|
1184
1281
|
if (!proxy) return;
|
|
1185
1282
|
proxy.invalidate();
|
|
1283
|
+
proxy.syncBox();
|
|
1186
1284
|
this.canvas.requestRenderAll();
|
|
1187
1285
|
}
|
|
1188
1286
|
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
@@ -1192,7 +1290,7 @@ var PatternManager = class {
|
|
|
1192
1290
|
this.attach(layer, state.config);
|
|
1193
1291
|
}
|
|
1194
1292
|
dispose() {
|
|
1195
|
-
this.canvas.off("object:modified", this.
|
|
1293
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
1196
1294
|
this.canvas.off("text:changed", this.onSourceModified);
|
|
1197
1295
|
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1198
1296
|
this.clearProxies();
|
|
@@ -1203,16 +1301,37 @@ var PatternManager = class {
|
|
|
1203
1301
|
this.proxies.clear();
|
|
1204
1302
|
}
|
|
1205
1303
|
attach(layer, config) {
|
|
1206
|
-
const proxy = new TiledPatternObject(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
this.canvas.getWidth(),
|
|
1210
|
-
this.canvas.getHeight()
|
|
1211
|
-
);
|
|
1212
|
-
layer.fabricObject.set({ opacity: 0 });
|
|
1304
|
+
const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
|
|
1305
|
+
const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
|
|
1306
|
+
layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
|
|
1213
1307
|
this.proxies.set(layer.id, proxy);
|
|
1214
1308
|
this.layers.setRenderProxy(layer.id, proxy);
|
|
1309
|
+
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1310
|
+
this.canvas.requestRenderAll();
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1314
|
+
* position becomes the grid origin (carried by the source), scale becomes the
|
|
1315
|
+
* tile scale, rotation becomes the pattern angle. Read the live transform
|
|
1316
|
+
* first — writing the config re-fits the box and destroys it.
|
|
1317
|
+
*/
|
|
1318
|
+
commitGesture(proxy) {
|
|
1319
|
+
const layer = this.layers.findByObject(proxy);
|
|
1320
|
+
const state = layer?.meta.pattern;
|
|
1321
|
+
if (!layer || !state) return;
|
|
1322
|
+
const origin = proxy.liveOrigin();
|
|
1323
|
+
const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
|
|
1324
|
+
const angle = normalizeAngle(proxy.liveAngle());
|
|
1325
|
+
layer.fabricObject.setPositionByOrigin(origin, "center", "center");
|
|
1326
|
+
layer.fabricObject.setCoords();
|
|
1327
|
+
const config = { ...state.config, scale, angle };
|
|
1328
|
+
this.layers.setMeta(layer.id, { pattern: { ...state, config } });
|
|
1329
|
+
proxy.setConfig(config);
|
|
1215
1330
|
this.canvas.requestRenderAll();
|
|
1331
|
+
this.history.save();
|
|
1332
|
+
}
|
|
1333
|
+
area() {
|
|
1334
|
+
return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
|
|
1216
1335
|
}
|
|
1217
1336
|
detach(layerId) {
|
|
1218
1337
|
const proxy = this.proxies.get(layerId);
|
|
@@ -1230,6 +1349,15 @@ var PatternManager = class {
|
|
|
1230
1349
|
if (layer) this.invalidate(layer.id);
|
|
1231
1350
|
}
|
|
1232
1351
|
};
|
|
1352
|
+
function clamp2(value, min, max) {
|
|
1353
|
+
if (!Number.isFinite(value)) return min;
|
|
1354
|
+
return Math.max(min, Math.min(max, value));
|
|
1355
|
+
}
|
|
1356
|
+
function normalizeAngle(angle) {
|
|
1357
|
+
if (!Number.isFinite(angle)) return 0;
|
|
1358
|
+
const wrapped = (angle % 360 + 360) % 360;
|
|
1359
|
+
return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
|
|
1360
|
+
}
|
|
1233
1361
|
function isLegacyState(state) {
|
|
1234
1362
|
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1235
1363
|
}
|
|
@@ -2529,6 +2657,737 @@ var MaskController = class {
|
|
|
2529
2657
|
}
|
|
2530
2658
|
};
|
|
2531
2659
|
|
|
2660
|
+
// src/masks/manager.ts
|
|
2661
|
+
import { Group as Group5 } from "fabric";
|
|
2662
|
+
|
|
2663
|
+
// src/masks/compose.ts
|
|
2664
|
+
import { Group, Rect as Rect2 } from "fabric";
|
|
2665
|
+
var MODE_OPERATION = {
|
|
2666
|
+
add: "source-over",
|
|
2667
|
+
subtract: "destination-out",
|
|
2668
|
+
intersect: "destination-in"
|
|
2669
|
+
};
|
|
2670
|
+
function neutralize(child) {
|
|
2671
|
+
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
2672
|
+
}
|
|
2673
|
+
function baseRect(box) {
|
|
2674
|
+
return new Rect2({
|
|
2675
|
+
left: box.left,
|
|
2676
|
+
top: box.top,
|
|
2677
|
+
width: Math.max(1, box.width),
|
|
2678
|
+
height: Math.max(1, box.height),
|
|
2679
|
+
originX: "left",
|
|
2680
|
+
originY: "top",
|
|
2681
|
+
fill: "#000000",
|
|
2682
|
+
objectCaching: false
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
function composeMaskGroup(children, entries, options) {
|
|
2686
|
+
if (children.length === 0) return void 0;
|
|
2687
|
+
children.forEach((child, index) => {
|
|
2688
|
+
const entry = entries[index];
|
|
2689
|
+
child.set({ objectCaching: false });
|
|
2690
|
+
if (!entry || !entry.visible) {
|
|
2691
|
+
neutralize(child);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
child.set({
|
|
2695
|
+
opacity: entry.opacity,
|
|
2696
|
+
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
const first = entries.find((entry) => entry.visible);
|
|
2700
|
+
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
2701
|
+
return new Group(withBase, {
|
|
2702
|
+
absolutePositioned: options.absolute,
|
|
2703
|
+
// Cached, so the children's compositing operations resolve against each
|
|
2704
|
+
// other instead of against the page underneath the mask.
|
|
2705
|
+
objectCaching: true,
|
|
2706
|
+
subTargetCheck: false,
|
|
2707
|
+
interactive: false
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
function needsAbsoluteSpace(entries) {
|
|
2711
|
+
return entries.some((entry) => !entry.linked);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// src/masks/edit.ts
|
|
2715
|
+
import { util as util4 } from "fabric";
|
|
2716
|
+
|
|
2717
|
+
// src/masks/space.ts
|
|
2718
|
+
import { util as util3 } from "fabric";
|
|
2719
|
+
function matrixOf(object) {
|
|
2720
|
+
return object.calcTransformMatrix();
|
|
2721
|
+
}
|
|
2722
|
+
function applyMatrix(object, matrix) {
|
|
2723
|
+
const decomposed = util3.qrDecompose(matrix);
|
|
2724
|
+
object.set({
|
|
2725
|
+
flipX: false,
|
|
2726
|
+
flipY: false,
|
|
2727
|
+
originX: "center",
|
|
2728
|
+
originY: "center",
|
|
2729
|
+
left: decomposed.translateX,
|
|
2730
|
+
top: decomposed.translateY,
|
|
2731
|
+
scaleX: decomposed.scaleX,
|
|
2732
|
+
scaleY: decomposed.scaleY,
|
|
2733
|
+
angle: decomposed.angle,
|
|
2734
|
+
skewX: decomposed.skewX,
|
|
2735
|
+
skewY: 0
|
|
2736
|
+
});
|
|
2737
|
+
object.setCoords();
|
|
2738
|
+
}
|
|
2739
|
+
function toCanvasSpace(object, host) {
|
|
2740
|
+
applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
2741
|
+
}
|
|
2742
|
+
function toHostSpace(object, host) {
|
|
2743
|
+
applyMatrix(
|
|
2744
|
+
object,
|
|
2745
|
+
util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object))
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
function relativeMatrix(object, host) {
|
|
2749
|
+
return util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object));
|
|
2750
|
+
}
|
|
2751
|
+
function applyRelativeMatrix(object, host, rel) {
|
|
2752
|
+
applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), rel));
|
|
2753
|
+
}
|
|
2754
|
+
function asObject(clip) {
|
|
2755
|
+
return clip;
|
|
2756
|
+
}
|
|
2757
|
+
function toMatrix(values) {
|
|
2758
|
+
if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
|
|
2759
|
+
return null;
|
|
2760
|
+
return [values[0], values[1], values[2], values[3], values[4], values[5]];
|
|
2761
|
+
}
|
|
2762
|
+
function fitToBox(object, box, zoom = 1) {
|
|
2763
|
+
const width = Math.max(1, box.width) * zoom;
|
|
2764
|
+
const height = Math.max(1, box.height) * zoom;
|
|
2765
|
+
object.set({
|
|
2766
|
+
originX: "center",
|
|
2767
|
+
originY: "center",
|
|
2768
|
+
angle: 0,
|
|
2769
|
+
skewX: 0,
|
|
2770
|
+
skewY: 0,
|
|
2771
|
+
left: box.left + box.width / 2,
|
|
2772
|
+
top: box.top + box.height / 2,
|
|
2773
|
+
scaleX: width / Math.max(1, object.width ?? 1),
|
|
2774
|
+
scaleY: height / Math.max(1, object.height ?? 1)
|
|
2775
|
+
});
|
|
2776
|
+
object.setCoords();
|
|
2777
|
+
}
|
|
2778
|
+
function unwrapGroup(group) {
|
|
2779
|
+
const children = group.removeAll();
|
|
2780
|
+
for (const child of children) child.setCoords();
|
|
2781
|
+
return children;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/masks/edit.ts
|
|
2785
|
+
var MaskEditController = class {
|
|
2786
|
+
constructor(canvas, onCommit) {
|
|
2787
|
+
this.canvas = canvas;
|
|
2788
|
+
this.onCommit = onCommit;
|
|
2789
|
+
}
|
|
2790
|
+
canvas;
|
|
2791
|
+
onCommit;
|
|
2792
|
+
handle = null;
|
|
2793
|
+
editing = null;
|
|
2794
|
+
child = null;
|
|
2795
|
+
group = null;
|
|
2796
|
+
active() {
|
|
2797
|
+
return this.editing;
|
|
2798
|
+
}
|
|
2799
|
+
/** Put a handle on the canvas for `child`, an object inside `group`. */
|
|
2800
|
+
async begin(edit, group, child) {
|
|
2801
|
+
this.end();
|
|
2802
|
+
const handle = await child.clone();
|
|
2803
|
+
applyMatrix(handle, matrixOf(child));
|
|
2804
|
+
handle.set({
|
|
2805
|
+
// Outline only: the mask's own fill would sit over the artwork it is
|
|
2806
|
+
// supposed to be revealing.
|
|
2807
|
+
fill: "rgba(0,0,0,0.001)",
|
|
2808
|
+
stroke: "#e0b055",
|
|
2809
|
+
strokeWidth: 2,
|
|
2810
|
+
strokeDashArray: [6, 4],
|
|
2811
|
+
strokeUniform: true,
|
|
2812
|
+
opacity: 1,
|
|
2813
|
+
selectable: true,
|
|
2814
|
+
evented: true,
|
|
2815
|
+
hasControls: true,
|
|
2816
|
+
hasBorders: true,
|
|
2817
|
+
objectCaching: false,
|
|
2818
|
+
excludeFromExport: true
|
|
2819
|
+
});
|
|
2820
|
+
this.handle = handle;
|
|
2821
|
+
this.editing = edit;
|
|
2822
|
+
this.child = child;
|
|
2823
|
+
this.group = group;
|
|
2824
|
+
this.canvas.add(handle);
|
|
2825
|
+
this.canvas.setActiveObject(handle);
|
|
2826
|
+
this.canvas.on("object:moving", this.onTransform);
|
|
2827
|
+
this.canvas.on("object:scaling", this.onTransform);
|
|
2828
|
+
this.canvas.on("object:rotating", this.onTransform);
|
|
2829
|
+
this.canvas.on("object:modified", this.onModified);
|
|
2830
|
+
this.canvas.requestRenderAll();
|
|
2831
|
+
return true;
|
|
2832
|
+
}
|
|
2833
|
+
/** Take the handle down. The geometry it wrote is already in the clip. */
|
|
2834
|
+
end() {
|
|
2835
|
+
if (!this.handle) return;
|
|
2836
|
+
this.canvas.off("object:moving", this.onTransform);
|
|
2837
|
+
this.canvas.off("object:scaling", this.onTransform);
|
|
2838
|
+
this.canvas.off("object:rotating", this.onTransform);
|
|
2839
|
+
this.canvas.off("object:modified", this.onModified);
|
|
2840
|
+
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
2841
|
+
this.canvas.remove(this.handle);
|
|
2842
|
+
this.handle = null;
|
|
2843
|
+
this.editing = null;
|
|
2844
|
+
this.child = null;
|
|
2845
|
+
this.group = null;
|
|
2846
|
+
this.canvas.requestRenderAll();
|
|
2847
|
+
}
|
|
2848
|
+
dispose() {
|
|
2849
|
+
this.end();
|
|
2850
|
+
}
|
|
2851
|
+
onTransform = (event) => {
|
|
2852
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
2853
|
+
this.write();
|
|
2854
|
+
};
|
|
2855
|
+
onModified = (event) => {
|
|
2856
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
2857
|
+
this.write();
|
|
2858
|
+
this.onCommit();
|
|
2859
|
+
};
|
|
2860
|
+
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
2861
|
+
write() {
|
|
2862
|
+
if (!this.handle || !this.child || !this.group) return;
|
|
2863
|
+
applyMatrix(
|
|
2864
|
+
this.child,
|
|
2865
|
+
util4.multiplyTransformMatrices(
|
|
2866
|
+
util4.invertTransform(matrixOf(this.group)),
|
|
2867
|
+
matrixOf(this.handle)
|
|
2868
|
+
)
|
|
2869
|
+
);
|
|
2870
|
+
this.group.dirty = true;
|
|
2871
|
+
this.group.set({ dirty: true });
|
|
2872
|
+
this.canvas.requestRenderAll();
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
// src/masks/store.ts
|
|
2877
|
+
import { Group as Group4 } from "fabric";
|
|
2878
|
+
|
|
2879
|
+
// src/masks/host.ts
|
|
2880
|
+
import { Rect as Rect3 } from "fabric";
|
|
2881
|
+
function findCanvasHost(layers) {
|
|
2882
|
+
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
2883
|
+
}
|
|
2884
|
+
function createCanvasHost(canvas, layers) {
|
|
2885
|
+
const rect = new Rect3({
|
|
2886
|
+
left: 0,
|
|
2887
|
+
top: 0,
|
|
2888
|
+
width: canvas.getWidth(),
|
|
2889
|
+
height: canvas.getHeight(),
|
|
2890
|
+
originX: "left",
|
|
2891
|
+
originY: "top",
|
|
2892
|
+
fill: "#000000",
|
|
2893
|
+
globalCompositeOperation: "destination-in",
|
|
2894
|
+
// It is edited from the layer list, never on the canvas: a drag box on
|
|
2895
|
+
// something that cannot be dragged only reads as broken.
|
|
2896
|
+
selectable: false,
|
|
2897
|
+
evented: false,
|
|
2898
|
+
hasControls: false,
|
|
2899
|
+
hasBorders: false,
|
|
2900
|
+
objectCaching: false
|
|
2901
|
+
});
|
|
2902
|
+
const layer = layers.add("mask", rect, "Design mask");
|
|
2903
|
+
layer.meta.canvasMask = true;
|
|
2904
|
+
layers.setLocked(layer.id, true);
|
|
2905
|
+
return layer;
|
|
2906
|
+
}
|
|
2907
|
+
function pinCanvasHost(layers) {
|
|
2908
|
+
const all = layers.getAll();
|
|
2909
|
+
const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
|
|
2910
|
+
if (hostIndex === -1) return false;
|
|
2911
|
+
let lastContent = -1;
|
|
2912
|
+
all.forEach((layer, index) => {
|
|
2913
|
+
if (layer.type !== "mask") lastContent = index;
|
|
2914
|
+
});
|
|
2915
|
+
if (hostIndex >= lastContent) return false;
|
|
2916
|
+
layers.reorder(all[hostIndex].id, all.length - 1);
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
function hostBoxOf(canvas, host, absolute) {
|
|
2920
|
+
if (!host) {
|
|
2921
|
+
return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
|
|
2922
|
+
}
|
|
2923
|
+
if (absolute) {
|
|
2924
|
+
host.setCoords();
|
|
2925
|
+
const rect = host.getBoundingRect();
|
|
2926
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
2927
|
+
}
|
|
2928
|
+
const width = Math.max(1, host.width ?? 1);
|
|
2929
|
+
const height = Math.max(1, host.height ?? 1);
|
|
2930
|
+
return { left: -width / 2, top: -height / 2, width, height };
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/masks/install.ts
|
|
2934
|
+
import { Group as Group3 } from "fabric";
|
|
2935
|
+
function convertSpace(host, sources, absolute) {
|
|
2936
|
+
const wasAbsolute = host.clipPath instanceof Group3 ? host.clipPath.absolutePositioned : absolute;
|
|
2937
|
+
if (absolute === wasAbsolute) return;
|
|
2938
|
+
for (const source of sources) {
|
|
2939
|
+
if (absolute) toCanvasSpace(source, host);
|
|
2940
|
+
else toHostSpace(source, host);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
function withRelativeTransforms(entries, sources, host, absolute) {
|
|
2944
|
+
return entries.map((entry, index) => {
|
|
2945
|
+
const source = sources[index];
|
|
2946
|
+
if (absolute && entry.linked && source) {
|
|
2947
|
+
return { ...entry, rel: [...relativeMatrix(source, host)] };
|
|
2948
|
+
}
|
|
2949
|
+
if (!entry.rel) return entry;
|
|
2950
|
+
const rest = { ...entry };
|
|
2951
|
+
delete rest.rel;
|
|
2952
|
+
return rest;
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
function installClip(host, entries, sources, box, absolute) {
|
|
2956
|
+
host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
|
|
2957
|
+
host.dirty = true;
|
|
2958
|
+
host.setCoords();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// src/masks/store.ts
|
|
2962
|
+
var CANVAS_MASK_TARGET = "canvas";
|
|
2963
|
+
var DEFAULT_ENTRY = {
|
|
2964
|
+
mode: "add",
|
|
2965
|
+
linked: true,
|
|
2966
|
+
visible: true,
|
|
2967
|
+
opacity: 1
|
|
2968
|
+
};
|
|
2969
|
+
var MaskStackStore = class {
|
|
2970
|
+
constructor(canvas, layers, history, events) {
|
|
2971
|
+
this.canvas = canvas;
|
|
2972
|
+
this.layers = layers;
|
|
2973
|
+
this.history = history;
|
|
2974
|
+
this.events = events;
|
|
2975
|
+
}
|
|
2976
|
+
canvas;
|
|
2977
|
+
layers;
|
|
2978
|
+
history;
|
|
2979
|
+
events;
|
|
2980
|
+
selected = null;
|
|
2981
|
+
pinning = false;
|
|
2982
|
+
list(target) {
|
|
2983
|
+
return this.hostLayer(target)?.meta.maskStack ?? [];
|
|
2984
|
+
}
|
|
2985
|
+
get(target, maskId) {
|
|
2986
|
+
return this.list(target).find((entry) => entry.id === maskId);
|
|
2987
|
+
}
|
|
2988
|
+
/** Every target that currently carries at least one mask. */
|
|
2989
|
+
targets() {
|
|
2990
|
+
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
2991
|
+
}
|
|
2992
|
+
/** The box a mask is fitted to, in the space the stack is composed in. */
|
|
2993
|
+
hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
|
|
2994
|
+
return hostBoxOf(this.canvas, this.host(target), absolute);
|
|
2995
|
+
}
|
|
2996
|
+
/** The host layer of a target, optionally creating the design overlay. */
|
|
2997
|
+
hostLayer(target, create = false) {
|
|
2998
|
+
if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
|
|
2999
|
+
const existing = findCanvasHost(this.layers);
|
|
3000
|
+
if (existing || !create) return existing;
|
|
3001
|
+
return createCanvasHost(this.canvas, this.layers);
|
|
3002
|
+
}
|
|
3003
|
+
host(target, create = false) {
|
|
3004
|
+
return this.hostLayer(target, create)?.fabricObject ?? null;
|
|
3005
|
+
}
|
|
3006
|
+
/** Re-pin the design overlay, guarding the reorder that re-triggers this. */
|
|
3007
|
+
pin() {
|
|
3008
|
+
if (this.pinning) return;
|
|
3009
|
+
this.pinning = true;
|
|
3010
|
+
try {
|
|
3011
|
+
pinCanvasHost(this.layers);
|
|
3012
|
+
} finally {
|
|
3013
|
+
this.pinning = false;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Take the current geometry back out of the composed clip, entry-aligned.
|
|
3018
|
+
*
|
|
3019
|
+
* The entry list is the authority on how to read the clip: with no entries the
|
|
3020
|
+
* clip predates the stack (a mask preset, or a single `clipPath` an older host
|
|
3021
|
+
* installed) and is one mask whole — including when it happens to be a group,
|
|
3022
|
+
* which is why this cannot just unwrap anything group-shaped.
|
|
3023
|
+
*/
|
|
3024
|
+
unwrap(target, host) {
|
|
3025
|
+
const clip = host.clipPath;
|
|
3026
|
+
if (!clip) return [];
|
|
3027
|
+
const entries = this.list(target);
|
|
3028
|
+
if (entries.length === 0 || !(clip instanceof Group4)) return [asObject(clip)];
|
|
3029
|
+
const children = unwrapGroup(clip);
|
|
3030
|
+
const extra = children.length - entries.length;
|
|
3031
|
+
return extra > 0 ? children.slice(extra) : children;
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
3035
|
+
* this, the first `add()` on an already-masked layer would compose a clip it
|
|
3036
|
+
* has no entry for and silently throw that mask away.
|
|
3037
|
+
*/
|
|
3038
|
+
entriesFor(target, sources) {
|
|
3039
|
+
const existing = this.list(target);
|
|
3040
|
+
if (existing.length > 0 || sources.length !== 1) return [...existing];
|
|
3041
|
+
const layer = this.hostLayer(target);
|
|
3042
|
+
const preset = layer?.meta.maskPreset;
|
|
3043
|
+
if (layer) {
|
|
3044
|
+
delete layer.meta.maskPreset;
|
|
3045
|
+
}
|
|
3046
|
+
return [
|
|
3047
|
+
{
|
|
3048
|
+
...DEFAULT_ENTRY,
|
|
3049
|
+
id: generateId(),
|
|
3050
|
+
name: typeof preset === "string" ? preset : "Mask",
|
|
3051
|
+
linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
|
|
3052
|
+
}
|
|
3053
|
+
];
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Install a stack: convert geometry into the space the stack needs, compose the
|
|
3057
|
+
* clip, store the entries, and commit one history checkpoint for the lot.
|
|
3058
|
+
*/
|
|
3059
|
+
commit(target, host, entries, sources, save = true, forceAbsolute = false) {
|
|
3060
|
+
const layer = this.hostLayer(target);
|
|
3061
|
+
if (!layer) return;
|
|
3062
|
+
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
3063
|
+
convertSpace(host, sources, absolute);
|
|
3064
|
+
const next = withRelativeTransforms(entries, sources, host, absolute);
|
|
3065
|
+
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
3066
|
+
if (next.length === 0) {
|
|
3067
|
+
delete layer.meta.maskStack;
|
|
3068
|
+
if (layer.meta.canvasMask) this.layers.remove(layer.id);
|
|
3069
|
+
} else {
|
|
3070
|
+
layer.meta.maskStack = next;
|
|
3071
|
+
}
|
|
3072
|
+
this.canvas.requestRenderAll();
|
|
3073
|
+
this.events.emit("masks:changed", { target });
|
|
3074
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
3075
|
+
if (save) this.history.save();
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
// src/masks/manager.ts
|
|
3080
|
+
var LayerMaskManager = class extends MaskStackStore {
|
|
3081
|
+
// Committing mid-drag would recompose the group the handle writes into and
|
|
3082
|
+
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
3083
|
+
edits = new MaskEditController(this.canvas, () => this.history.save());
|
|
3084
|
+
onLayersChanged = () => this.pin();
|
|
3085
|
+
// Selecting a layer means the user has moved on from the mask they had open;
|
|
3086
|
+
// leaving both selected would show mask controls for an unrelated layer.
|
|
3087
|
+
onLayerSelected = () => {
|
|
3088
|
+
if (this.selected) this.select(null, null);
|
|
3089
|
+
};
|
|
3090
|
+
onObjectModified = (event) => {
|
|
3091
|
+
const object = event.target;
|
|
3092
|
+
if (!object) return;
|
|
3093
|
+
const layer = this.layers.findByObject(object);
|
|
3094
|
+
if (layer) this.reflow(layer.id);
|
|
3095
|
+
};
|
|
3096
|
+
constructor(canvas, layers, history, events) {
|
|
3097
|
+
super(canvas, layers, history, events);
|
|
3098
|
+
this.events.on("layers:changed", this.onLayersChanged);
|
|
3099
|
+
this.events.on("layer:selected", this.onLayerSelected);
|
|
3100
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
3101
|
+
}
|
|
3102
|
+
dispose() {
|
|
3103
|
+
this.edits.dispose();
|
|
3104
|
+
this.events.off("layers:changed", this.onLayersChanged);
|
|
3105
|
+
this.events.off("layer:selected", this.onLayerSelected);
|
|
3106
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
3107
|
+
this.selected = null;
|
|
3108
|
+
}
|
|
3109
|
+
// ─── Geometry editing ────────────────────────────────
|
|
3110
|
+
/** The mask currently being dragged on the canvas, if any. */
|
|
3111
|
+
editing() {
|
|
3112
|
+
return this.edits.active();
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Put drag handles on one mask. The stack is composed in canvas space for the
|
|
3116
|
+
* duration — a linked mask would otherwise sit in the host's space, where the
|
|
3117
|
+
* handle's own canvas coordinates mean something else entirely. `endEdit`
|
|
3118
|
+
* returns it to whichever space its entries call for.
|
|
3119
|
+
*/
|
|
3120
|
+
async beginEdit(target, maskId) {
|
|
3121
|
+
const host = this.host(target);
|
|
3122
|
+
if (!host) return false;
|
|
3123
|
+
const entries = this.list(target);
|
|
3124
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3125
|
+
if (index === -1) return false;
|
|
3126
|
+
this.endEdit(false);
|
|
3127
|
+
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3128
|
+
const group = host.clipPath instanceof Group5 ? host.clipPath : null;
|
|
3129
|
+
if (!group) return false;
|
|
3130
|
+
const children = group.getObjects();
|
|
3131
|
+
const child = children[children.length - entries.length + index];
|
|
3132
|
+
if (!child) return false;
|
|
3133
|
+
this.select(target, maskId);
|
|
3134
|
+
return this.edits.begin({ target, maskId }, group, child);
|
|
3135
|
+
}
|
|
3136
|
+
/** Take the handles down and settle the stack back into its own space. */
|
|
3137
|
+
endEdit(save = true) {
|
|
3138
|
+
const editing = this.edits.active();
|
|
3139
|
+
this.edits.end();
|
|
3140
|
+
if (editing) this.rebuild(editing.target, save);
|
|
3141
|
+
}
|
|
3142
|
+
// ─── Selection ───────────────────────────────────────
|
|
3143
|
+
getSelected() {
|
|
3144
|
+
return this.selected;
|
|
3145
|
+
}
|
|
3146
|
+
select(target, maskId) {
|
|
3147
|
+
this.selected = target && maskId ? { target, maskId } : null;
|
|
3148
|
+
this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
|
|
3149
|
+
}
|
|
3150
|
+
// ─── Mutations ───────────────────────────────────────
|
|
3151
|
+
add(target, object, options = {}) {
|
|
3152
|
+
const host = this.host(target, true);
|
|
3153
|
+
if (!host) return null;
|
|
3154
|
+
const sources = this.unwrap(target, host);
|
|
3155
|
+
const entries = this.entriesFor(target, sources);
|
|
3156
|
+
const entry = {
|
|
3157
|
+
...DEFAULT_ENTRY,
|
|
3158
|
+
id: generateId(),
|
|
3159
|
+
name: options.name ?? "Mask",
|
|
3160
|
+
...options.mode ? { mode: options.mode } : {},
|
|
3161
|
+
...options.linked === void 0 ? {} : { linked: options.linked },
|
|
3162
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3163
|
+
};
|
|
3164
|
+
if (target === CANVAS_MASK_TARGET) entry.linked = false;
|
|
3165
|
+
if (options.fit !== false) {
|
|
3166
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3167
|
+
}
|
|
3168
|
+
sources.push(object);
|
|
3169
|
+
entries.push(entry);
|
|
3170
|
+
this.commit(target, host, entries, sources);
|
|
3171
|
+
return entry;
|
|
3172
|
+
}
|
|
3173
|
+
/** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
|
|
3174
|
+
replaceGeometry(target, maskId, object, options = {}) {
|
|
3175
|
+
const host = this.host(target);
|
|
3176
|
+
if (!host) return false;
|
|
3177
|
+
const entries = [...this.list(target)];
|
|
3178
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3179
|
+
if (index === -1) return false;
|
|
3180
|
+
const sources = this.unwrap(target, host);
|
|
3181
|
+
if (options.fit !== false) {
|
|
3182
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3183
|
+
}
|
|
3184
|
+
sources[index] = object;
|
|
3185
|
+
entries[index] = {
|
|
3186
|
+
...entries[index],
|
|
3187
|
+
...options.name ? { name: options.name } : {},
|
|
3188
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3189
|
+
};
|
|
3190
|
+
this.commit(target, host, entries, sources);
|
|
3191
|
+
return true;
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
|
|
3195
|
+
* geometry it already has. This is what a zoom control drives: re-picking the
|
|
3196
|
+
* mask would cost another render of a generated alpha map just to resize it.
|
|
3197
|
+
*/
|
|
3198
|
+
refit(target, maskId, options = {}) {
|
|
3199
|
+
const host = this.host(target);
|
|
3200
|
+
if (!host) return false;
|
|
3201
|
+
const entries = [...this.list(target)];
|
|
3202
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3203
|
+
if (index === -1) return false;
|
|
3204
|
+
const sources = this.unwrap(target, host);
|
|
3205
|
+
const source = sources[index];
|
|
3206
|
+
if (!source) return false;
|
|
3207
|
+
fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3208
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
3209
|
+
return true;
|
|
3210
|
+
}
|
|
3211
|
+
remove(target, maskId) {
|
|
3212
|
+
const host = this.host(target);
|
|
3213
|
+
if (!host) return false;
|
|
3214
|
+
const entries = [...this.list(target)];
|
|
3215
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3216
|
+
if (index === -1) return false;
|
|
3217
|
+
const sources = this.unwrap(target, host);
|
|
3218
|
+
entries.splice(index, 1);
|
|
3219
|
+
sources.splice(index, 1);
|
|
3220
|
+
if (this.selected?.maskId === maskId) this.select(null, null);
|
|
3221
|
+
this.commit(target, host, entries, sources);
|
|
3222
|
+
return true;
|
|
3223
|
+
}
|
|
3224
|
+
clear(target) {
|
|
3225
|
+
const host = this.host(target);
|
|
3226
|
+
if (!host) return false;
|
|
3227
|
+
if (this.list(target).length === 0) return false;
|
|
3228
|
+
if (this.selected?.target === target) this.select(null, null);
|
|
3229
|
+
this.commit(target, host, [], []);
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
reorder(target, maskId, index) {
|
|
3233
|
+
const host = this.host(target);
|
|
3234
|
+
if (!host) return false;
|
|
3235
|
+
const entries = [...this.list(target)];
|
|
3236
|
+
const from = entries.findIndex((entry2) => entry2.id === maskId);
|
|
3237
|
+
if (from === -1) return false;
|
|
3238
|
+
const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
|
|
3239
|
+
if (from === to) return false;
|
|
3240
|
+
const sources = this.unwrap(target, host);
|
|
3241
|
+
const [entry] = entries.splice(from, 1);
|
|
3242
|
+
const [source] = sources.splice(from, 1);
|
|
3243
|
+
entries.splice(to, 0, entry);
|
|
3244
|
+
sources.splice(to, 0, source);
|
|
3245
|
+
this.commit(target, host, entries, sources);
|
|
3246
|
+
return true;
|
|
3247
|
+
}
|
|
3248
|
+
/** Replace a mask's host metadata (which preset or generator produced it). */
|
|
3249
|
+
setMeta(target, maskId, meta) {
|
|
3250
|
+
return this.patch(target, maskId, () => ({ meta }));
|
|
3251
|
+
}
|
|
3252
|
+
setMode(target, maskId, mode) {
|
|
3253
|
+
return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
|
|
3254
|
+
}
|
|
3255
|
+
setVisible(target, maskId, visible) {
|
|
3256
|
+
return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
|
|
3257
|
+
}
|
|
3258
|
+
setOpacity(target, maskId, opacity) {
|
|
3259
|
+
if (!Number.isFinite(opacity)) return false;
|
|
3260
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
3261
|
+
return this.patch(
|
|
3262
|
+
target,
|
|
3263
|
+
maskId,
|
|
3264
|
+
(entry) => entry.opacity === next ? null : { opacity: next }
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
setName(target, maskId, name) {
|
|
3268
|
+
const trimmed = name.trim();
|
|
3269
|
+
if (!trimmed) return false;
|
|
3270
|
+
return this.patch(
|
|
3271
|
+
target,
|
|
3272
|
+
maskId,
|
|
3273
|
+
(entry) => entry.name === trimmed ? null : { name: trimmed }
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Link or unlink one mask. Unlinking pins it where it currently appears;
|
|
3278
|
+
* re-linking records its position relative to the host so later host moves
|
|
3279
|
+
* carry it along.
|
|
3280
|
+
*/
|
|
3281
|
+
setLinked(target, maskId, linked) {
|
|
3282
|
+
if (target === CANVAS_MASK_TARGET) return false;
|
|
3283
|
+
const host = this.host(target);
|
|
3284
|
+
if (!host) return false;
|
|
3285
|
+
const entries = [...this.list(target)];
|
|
3286
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3287
|
+
if (index === -1 || entries[index].linked === linked) return false;
|
|
3288
|
+
const sources = this.unwrap(target, host);
|
|
3289
|
+
entries[index] = { ...entries[index], linked };
|
|
3290
|
+
this.commit(target, host, entries, sources);
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Consume a layer, turning its artwork into a mask. Without an explicit target
|
|
3295
|
+
* it masks the layer directly beneath it, and the bottom layer masks the whole
|
|
3296
|
+
* design — there is nothing under it to clip.
|
|
3297
|
+
*/
|
|
3298
|
+
convertLayer(layerId, target) {
|
|
3299
|
+
const layer = this.layers.get(layerId);
|
|
3300
|
+
if (!layer || layer.meta.canvasMask) return null;
|
|
3301
|
+
const ordered = this.layers.getAll();
|
|
3302
|
+
const index = ordered.findIndex((candidate) => candidate.id === layerId);
|
|
3303
|
+
const below = index > 0 ? ordered[index - 1] : void 0;
|
|
3304
|
+
const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
|
|
3305
|
+
if (resolved === layerId) return null;
|
|
3306
|
+
const object = layer.fabricObject;
|
|
3307
|
+
if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
|
|
3308
|
+
this.history.beginTransaction();
|
|
3309
|
+
try {
|
|
3310
|
+
if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
|
|
3311
|
+
this.canvas.remove(object);
|
|
3312
|
+
this.layers.remove(layerId);
|
|
3313
|
+
const entry = this.add(resolved, object, { name: layer.name, fit: false });
|
|
3314
|
+
return entry ? { target: resolved, entry } : null;
|
|
3315
|
+
} finally {
|
|
3316
|
+
this.history.endTransaction();
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
// ─── Rebuild / restore ───────────────────────────────
|
|
3320
|
+
/** Recompose one stack's clip from the geometry it already holds. */
|
|
3321
|
+
rebuild(target, save = false) {
|
|
3322
|
+
const host = this.host(target);
|
|
3323
|
+
if (!host) return;
|
|
3324
|
+
const sources = this.unwrap(target, host);
|
|
3325
|
+
if (sources.length === 0) return;
|
|
3326
|
+
this.commit(target, host, this.entriesFor(target, sources), sources, save);
|
|
3327
|
+
}
|
|
3328
|
+
/** Re-derive linked masks after the host moved (canvas-space stacks only). */
|
|
3329
|
+
reflow(target) {
|
|
3330
|
+
const host = this.host(target);
|
|
3331
|
+
if (!host) return;
|
|
3332
|
+
const entries = this.list(target);
|
|
3333
|
+
if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
|
|
3334
|
+
if (!entries.some((entry) => entry.linked && entry.rel)) return;
|
|
3335
|
+
const sources = this.unwrap(target, host);
|
|
3336
|
+
entries.forEach((entry, index) => {
|
|
3337
|
+
const source = sources[index];
|
|
3338
|
+
const rel = toMatrix(entry.rel);
|
|
3339
|
+
if (!source || !entry.linked || !rel) return;
|
|
3340
|
+
applyRelativeMatrix(source, host, rel);
|
|
3341
|
+
});
|
|
3342
|
+
this.commit(target, host, [...entries], sources);
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Adopt a clip this manager did not install — a host's own single `clipPath` —
|
|
3346
|
+
* as a one-entry stack, so it shows up in the UI as a mask row before anything
|
|
3347
|
+
* is added to it. A mask preset is left alone unless asked for by name: it is
|
|
3348
|
+
* still owned by `MaskPresetManager` until a stack operation takes it over.
|
|
3349
|
+
*/
|
|
3350
|
+
adopt(target, name) {
|
|
3351
|
+
const layer = this.hostLayer(target);
|
|
3352
|
+
const clip = layer?.fabricObject.clipPath;
|
|
3353
|
+
if (!layer || !clip || layer.meta.pattern) return null;
|
|
3354
|
+
if ((layer.meta.maskStack ?? []).length > 0) return null;
|
|
3355
|
+
const entry = {
|
|
3356
|
+
...DEFAULT_ENTRY,
|
|
3357
|
+
id: generateId(),
|
|
3358
|
+
name: name ?? "Mask",
|
|
3359
|
+
linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
|
|
3360
|
+
};
|
|
3361
|
+
layer.meta.maskStack = [entry];
|
|
3362
|
+
delete layer.meta.maskPreset;
|
|
3363
|
+
this.rebuild(target);
|
|
3364
|
+
return entry;
|
|
3365
|
+
}
|
|
3366
|
+
/** After a state restore: re-pin the design overlay and recompose every stack. */
|
|
3367
|
+
refreshAll() {
|
|
3368
|
+
this.selected = null;
|
|
3369
|
+
this.pin();
|
|
3370
|
+
for (const layer of this.layers.getAll()) {
|
|
3371
|
+
if ((layer.meta.maskStack ?? []).length === 0) continue;
|
|
3372
|
+
this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
// ─── Internals ───────────────────────────────────────
|
|
3376
|
+
patch(target, maskId, change) {
|
|
3377
|
+
const host = this.host(target);
|
|
3378
|
+
if (!host) return false;
|
|
3379
|
+
const entries = [...this.list(target)];
|
|
3380
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3381
|
+
if (index === -1) return false;
|
|
3382
|
+
const patch = change(entries[index]);
|
|
3383
|
+
if (!patch) return false;
|
|
3384
|
+
const sources = this.unwrap(target, host);
|
|
3385
|
+
entries[index] = { ...entries[index], ...patch };
|
|
3386
|
+
this.commit(target, host, entries, sources);
|
|
3387
|
+
return true;
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
|
|
2532
3391
|
// src/editor.ts
|
|
2533
3392
|
var MIN_ZOOM = 0.1;
|
|
2534
3393
|
var MAX_ZOOM = 8;
|
|
@@ -2546,6 +3405,8 @@ var CanvasEditor = class {
|
|
|
2546
3405
|
patterns;
|
|
2547
3406
|
curves;
|
|
2548
3407
|
maskPresets;
|
|
3408
|
+
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
3409
|
+
layerMasks;
|
|
2549
3410
|
fonts;
|
|
2550
3411
|
licensing;
|
|
2551
3412
|
pages;
|
|
@@ -2594,6 +3455,7 @@ var CanvasEditor = class {
|
|
|
2594
3455
|
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2595
3456
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2596
3457
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
3458
|
+
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
2597
3459
|
this.setupCanvasEvents();
|
|
2598
3460
|
this.refreshSelectionStyle();
|
|
2599
3461
|
this.history.saveImmediate();
|
|
@@ -2609,6 +3471,7 @@ var CanvasEditor = class {
|
|
|
2609
3471
|
{ originX: "left", originY: "top", ...options }
|
|
2610
3472
|
);
|
|
2611
3473
|
const layer = this.layers.add("image", img);
|
|
3474
|
+
this.layers.select(layer.id);
|
|
2612
3475
|
this.history.save();
|
|
2613
3476
|
return layer;
|
|
2614
3477
|
} catch (error) {
|
|
@@ -2668,12 +3531,14 @@ var CanvasEditor = class {
|
|
|
2668
3531
|
...options
|
|
2669
3532
|
});
|
|
2670
3533
|
const layer = this.layers.add("text", textbox);
|
|
3534
|
+
this.layers.select(layer.id);
|
|
2671
3535
|
this.history.save();
|
|
2672
3536
|
return layer;
|
|
2673
3537
|
}
|
|
2674
3538
|
addShape(plugin, options) {
|
|
2675
3539
|
const obj = plugin.create(options);
|
|
2676
3540
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
3541
|
+
this.layers.select(layer.id);
|
|
2677
3542
|
this.history.save();
|
|
2678
3543
|
return layer;
|
|
2679
3544
|
}
|
|
@@ -2699,7 +3564,7 @@ var CanvasEditor = class {
|
|
|
2699
3564
|
const { objects, options } = await loadSVGFromString(resolved);
|
|
2700
3565
|
const validObjects = objects.filter((object) => object !== null);
|
|
2701
3566
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2702
|
-
const group =
|
|
3567
|
+
const group = util5.groupSVGElements(validObjects, options);
|
|
2703
3568
|
group.set({
|
|
2704
3569
|
left: this.canvas.getWidth() / 2,
|
|
2705
3570
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2707,6 +3572,7 @@ var CanvasEditor = class {
|
|
|
2707
3572
|
originY: "center"
|
|
2708
3573
|
});
|
|
2709
3574
|
const layer = this.layers.add("template", group, template.name);
|
|
3575
|
+
this.layers.select(layer.id);
|
|
2710
3576
|
this.history.save();
|
|
2711
3577
|
return layer;
|
|
2712
3578
|
}
|
|
@@ -2758,7 +3624,7 @@ var CanvasEditor = class {
|
|
|
2758
3624
|
opacity: layer.opacity
|
|
2759
3625
|
});
|
|
2760
3626
|
this.patterns.attachTo(copy);
|
|
2761
|
-
this.
|
|
3627
|
+
this.layers.select(copy.id);
|
|
2762
3628
|
this.canvas.requestRenderAll();
|
|
2763
3629
|
this.history.save();
|
|
2764
3630
|
return copy;
|
|
@@ -2791,7 +3657,7 @@ var CanvasEditor = class {
|
|
|
2791
3657
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2792
3658
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2793
3659
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2794
|
-
const group = new
|
|
3660
|
+
const group = new Group6(objects);
|
|
2795
3661
|
const grouped = this.layers.add("group", group, name);
|
|
2796
3662
|
grouped.meta.groupChildren = childData;
|
|
2797
3663
|
this.layers.select(grouped.id);
|
|
@@ -2806,11 +3672,9 @@ var CanvasEditor = class {
|
|
|
2806
3672
|
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
2807
3673
|
const group = grouped.fabricObject;
|
|
2808
3674
|
return this.history.transaction(() => {
|
|
2809
|
-
const transform = group.calcTransformMatrix();
|
|
2810
3675
|
const objects = group.removeAll();
|
|
2811
3676
|
this.layers.remove(id);
|
|
2812
3677
|
const restored = objects.map((object, index) => {
|
|
2813
|
-
util3.addTransformToObject(object, transform);
|
|
2814
3678
|
object.setCoords();
|
|
2815
3679
|
const data = childData[index];
|
|
2816
3680
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -2838,6 +3702,7 @@ var CanvasEditor = class {
|
|
|
2838
3702
|
try {
|
|
2839
3703
|
await deserializeEditor(this, state);
|
|
2840
3704
|
await this.patterns.rehydrateAll();
|
|
3705
|
+
this.layerMasks.refreshAll();
|
|
2841
3706
|
} catch (error) {
|
|
2842
3707
|
if (!managedByHistory) {
|
|
2843
3708
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3410,6 +4275,7 @@ var CanvasEditor = class {
|
|
|
3410
4275
|
// ─── Cleanup ────────────────────────────────────────
|
|
3411
4276
|
dispose() {
|
|
3412
4277
|
this.masks.dispose();
|
|
4278
|
+
this.layerMasks.dispose();
|
|
3413
4279
|
this.snapping.dispose();
|
|
3414
4280
|
this.crop.dispose();
|
|
3415
4281
|
this.history.dispose();
|
|
@@ -3532,6 +4398,7 @@ var AnnotationOverlay = class {
|
|
|
3532
4398
|
};
|
|
3533
4399
|
export {
|
|
3534
4400
|
AnnotationOverlay,
|
|
4401
|
+
CANVAS_MASK_TARGET,
|
|
3535
4402
|
CANVAS_SIZE_PRESETS,
|
|
3536
4403
|
CanvasEditor,
|
|
3537
4404
|
CropController,
|
|
@@ -3544,6 +4411,7 @@ export {
|
|
|
3544
4411
|
HistoryManager,
|
|
3545
4412
|
Layer,
|
|
3546
4413
|
LayerManager,
|
|
4414
|
+
LayerMaskManager,
|
|
3547
4415
|
LicenseManager,
|
|
3548
4416
|
MaskController,
|
|
3549
4417
|
MaskPresetManager,
|
|
@@ -3565,6 +4433,7 @@ export {
|
|
|
3565
4433
|
buildCurvePathData,
|
|
3566
4434
|
clamp,
|
|
3567
4435
|
clearTextureMaskCache,
|
|
4436
|
+
composeMaskGroup,
|
|
3568
4437
|
computeCoverPlacement,
|
|
3569
4438
|
computePrintAreaClip,
|
|
3570
4439
|
computeTilePositions,
|
|
@@ -3577,11 +4446,13 @@ export {
|
|
|
3577
4446
|
exportPNG,
|
|
3578
4447
|
exportPrintArea,
|
|
3579
4448
|
exportSVG,
|
|
4449
|
+
fitToBox,
|
|
3580
4450
|
generateId,
|
|
3581
4451
|
isCssColor,
|
|
3582
4452
|
isMaskPresetId,
|
|
3583
4453
|
isShapeMaskId,
|
|
3584
4454
|
isTextureMaskId,
|
|
4455
|
+
needsAbsoluteSpace,
|
|
3585
4456
|
readLayerShadow,
|
|
3586
4457
|
renderTextureMask,
|
|
3587
4458
|
resetTransform,
|
|
@@ -3589,6 +4460,9 @@ export {
|
|
|
3589
4460
|
round2,
|
|
3590
4461
|
sanitizeSvg,
|
|
3591
4462
|
serializeEditor,
|
|
3592
|
-
shapeMaskPathData
|
|
4463
|
+
shapeMaskPathData,
|
|
4464
|
+
toCanvasSpace,
|
|
4465
|
+
toHostSpace,
|
|
4466
|
+
unwrapGroup
|
|
3593
4467
|
};
|
|
3594
4468
|
//# sourceMappingURL=index.mjs.map
|