@overtone-art/canvas-editor-core 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +339 -25
- package/dist/index.d.ts +339 -25
- package/dist/index.global.js +53 -53
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1053 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1034 -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;
|
|
@@ -879,8 +886,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
|
879
886
|
}
|
|
880
887
|
return placements;
|
|
881
888
|
}
|
|
882
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
889
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
|
|
883
890
|
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
891
|
+
const drawW = draw && draw.width > 0 ? draw.width : baseW;
|
|
892
|
+
const drawH = draw && draw.height > 0 ? draw.height : baseH;
|
|
884
893
|
ctx.save();
|
|
885
894
|
ctx.translate(anchor.x, anchor.y);
|
|
886
895
|
ctx.rotate(config.angle * Math.PI / 180);
|
|
@@ -888,7 +897,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
|
888
897
|
ctx.save();
|
|
889
898
|
ctx.translate(tile.x, tile.y);
|
|
890
899
|
ctx.rotate(tile.rotation);
|
|
891
|
-
ctx.drawImage(img, -
|
|
900
|
+
ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
|
|
892
901
|
ctx.restore();
|
|
893
902
|
}
|
|
894
903
|
ctx.restore();
|
|
@@ -910,46 +919,43 @@ function mod2(n) {
|
|
|
910
919
|
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
911
920
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
912
921
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
922
|
+
var TILE_BLEED_DEVICE_PX = 2;
|
|
913
923
|
var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
914
924
|
static type = "TiledPattern";
|
|
915
925
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
916
926
|
source;
|
|
917
927
|
config;
|
|
928
|
+
/** The print area this pattern fills, in canvas units. */
|
|
929
|
+
area;
|
|
918
930
|
snapshotEl = null;
|
|
919
931
|
snapshotScale = 0;
|
|
920
|
-
constructor(source, config,
|
|
932
|
+
constructor(source, config, area) {
|
|
921
933
|
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,
|
|
934
|
+
// Centre origin: the box tracks a tile, and a tile is placed by its centre.
|
|
935
|
+
originX: "center",
|
|
936
|
+
originY: "center",
|
|
935
937
|
objectCaching: false,
|
|
936
|
-
|
|
937
|
-
|
|
938
|
+
// Side handles would imply a non-uniform tile scale; the config has one.
|
|
939
|
+
lockScalingFlip: true
|
|
938
940
|
});
|
|
939
941
|
this.source = source;
|
|
940
942
|
this.config = config;
|
|
943
|
+
this.area = area;
|
|
944
|
+
this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
|
|
945
|
+
this.syncBox();
|
|
941
946
|
}
|
|
942
|
-
/** Swap in a new config
|
|
947
|
+
/** Swap in a new config and re-fit the box to the tile it now describes. */
|
|
943
948
|
setConfig(config) {
|
|
944
949
|
this.config = config;
|
|
945
|
-
this.
|
|
950
|
+
this.syncBox();
|
|
946
951
|
}
|
|
947
|
-
/**
|
|
948
|
-
setArea(
|
|
949
|
-
this.
|
|
950
|
-
this.
|
|
952
|
+
/** Re-fit to a new print area (canvas resize). */
|
|
953
|
+
setArea(area) {
|
|
954
|
+
this.area = area;
|
|
955
|
+
this.invalidate();
|
|
956
|
+
this.syncBox();
|
|
951
957
|
}
|
|
952
|
-
/** Drop the cached source snapshot (
|
|
958
|
+
/** Drop the cached source snapshot (the source was edited). */
|
|
953
959
|
invalidate() {
|
|
954
960
|
this.snapshotEl = null;
|
|
955
961
|
this.snapshotScale = 0;
|
|
@@ -960,40 +966,118 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
960
966
|
this.snapshotEl = null;
|
|
961
967
|
this.snapshotScale = 0;
|
|
962
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* Put the box back on the anchor tile: the tile's size, the pattern angle, and
|
|
971
|
+
* the grid origin displaced by the configured phase shift. Resets any live
|
|
972
|
+
* gesture scale, so it must run only once that gesture has been folded in.
|
|
973
|
+
*/
|
|
974
|
+
syncBox() {
|
|
975
|
+
const { tileW, tileH } = this.baseTile();
|
|
976
|
+
const origin = this.source.getCenterPoint();
|
|
977
|
+
const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
|
|
978
|
+
this.set({
|
|
979
|
+
left: anchor.x,
|
|
980
|
+
top: anchor.y,
|
|
981
|
+
width: tileW,
|
|
982
|
+
height: tileH,
|
|
983
|
+
scaleX: 1,
|
|
984
|
+
scaleY: 1,
|
|
985
|
+
angle: this.config.angle
|
|
986
|
+
});
|
|
987
|
+
this.setCoords();
|
|
988
|
+
this.dirty = true;
|
|
989
|
+
}
|
|
990
|
+
/** Grid rotation in play right now — the live handle during a rotate gesture. */
|
|
991
|
+
liveAngle() {
|
|
992
|
+
return this.angle ?? 0;
|
|
993
|
+
}
|
|
994
|
+
/** Tile scale in play right now, as a config percentage. */
|
|
995
|
+
liveScale() {
|
|
996
|
+
return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Where the source's centre would have to sit for the box to stay put — i.e.
|
|
1000
|
+
* the grid origin implied by a drag. `PatternManager` moves the source there.
|
|
1001
|
+
*/
|
|
1002
|
+
liveOrigin() {
|
|
1003
|
+
const { tileW, tileH } = this.liveTile();
|
|
1004
|
+
const centre = this.getCenterPoint();
|
|
1005
|
+
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1006
|
+
return new Point(centre.x - shift.x, centre.y - shift.y);
|
|
1007
|
+
}
|
|
963
1008
|
_render(ctx) {
|
|
964
|
-
const width = this.
|
|
965
|
-
const height = this.height ?? 0;
|
|
1009
|
+
const { width, height } = this.area;
|
|
966
1010
|
if (width <= 0 || height <= 0) return;
|
|
967
|
-
const
|
|
968
|
-
const
|
|
1011
|
+
const { tileW, tileH } = this.liveTile();
|
|
1012
|
+
const scale = contextScale(ctx);
|
|
1013
|
+
const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
|
|
969
1014
|
if (!snapshot) return;
|
|
970
|
-
const
|
|
1015
|
+
const centre = this.getCenterPoint();
|
|
1016
|
+
const angle = this.liveAngle();
|
|
971
1017
|
ctx.save();
|
|
972
|
-
ctx.
|
|
973
|
-
|
|
1018
|
+
ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
|
|
1019
|
+
ctx.rotate(-angle * Math.PI / 180);
|
|
1020
|
+
ctx.translate(-centre.x, -centre.y);
|
|
1021
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1022
|
+
const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
|
|
1023
|
+
const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
|
|
1024
|
+
drawTiles(
|
|
1025
|
+
ctx,
|
|
1026
|
+
snapshot,
|
|
1027
|
+
config,
|
|
1028
|
+
width,
|
|
1029
|
+
height,
|
|
1030
|
+
tileW,
|
|
1031
|
+
tileH,
|
|
1032
|
+
origin,
|
|
1033
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
|
|
1034
|
+
);
|
|
974
1035
|
ctx.restore();
|
|
975
1036
|
}
|
|
976
1037
|
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
977
1038
|
_toSVG() {
|
|
978
|
-
const width = this.
|
|
979
|
-
const height = this.height ?? 0;
|
|
1039
|
+
const { width, height } = this.area;
|
|
980
1040
|
if (width <= 0 || height <= 0) return [];
|
|
981
1041
|
const el = document.createElement("canvas");
|
|
982
1042
|
el.width = Math.max(1, Math.round(width));
|
|
983
1043
|
el.height = Math.max(1, Math.round(height));
|
|
984
1044
|
const ctx = el.getContext("2d");
|
|
985
1045
|
if (!ctx) return [];
|
|
986
|
-
const
|
|
987
|
-
const snapshot = this.ensureSnapshot(
|
|
1046
|
+
const { tileW, tileH } = this.liveTile();
|
|
1047
|
+
const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
|
|
988
1048
|
if (!snapshot) return [];
|
|
989
|
-
const
|
|
990
|
-
|
|
1049
|
+
const centre = this.getCenterPoint();
|
|
1050
|
+
const angle = this.liveAngle();
|
|
1051
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1052
|
+
drawTiles(
|
|
1053
|
+
ctx,
|
|
1054
|
+
snapshot,
|
|
1055
|
+
{ ...this.config, angle, offsetX: 0, offsetY: 0 },
|
|
1056
|
+
width,
|
|
1057
|
+
height,
|
|
1058
|
+
tileW,
|
|
1059
|
+
tileH,
|
|
1060
|
+
{ x: centre.x - shift.x, y: centre.y - shift.y },
|
|
1061
|
+
// The fallback raster is built at 1:1, so a device pixel is a canvas unit.
|
|
1062
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
|
|
1063
|
+
);
|
|
991
1064
|
return [
|
|
992
|
-
`<
|
|
993
|
-
|
|
1065
|
+
`<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
|
|
1066
|
+
`<image x="0" y="0" width="${width}" height="${height}" `,
|
|
1067
|
+
`xlink:href="${el.toDataURL("image/png")}"></image></g>
|
|
994
1068
|
`
|
|
995
1069
|
];
|
|
996
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1073
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1074
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1075
|
+
*/
|
|
1076
|
+
toObject() {
|
|
1077
|
+
const plain = super.toObject();
|
|
1078
|
+
delete plain.source;
|
|
1079
|
+
return plain;
|
|
1080
|
+
}
|
|
997
1081
|
/**
|
|
998
1082
|
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
999
1083
|
* registry, which cannot carry a live source reference. Export paths clone
|
|
@@ -1001,46 +1085,110 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1001
1085
|
* tiling. The copy shares the source (it only ever reads from it).
|
|
1002
1086
|
*/
|
|
1003
1087
|
clone() {
|
|
1004
|
-
const copy = new _TiledPatternObject(
|
|
1005
|
-
this.source,
|
|
1006
|
-
this.config,
|
|
1007
|
-
this.width ?? 0,
|
|
1008
|
-
this.height ?? 0
|
|
1009
|
-
);
|
|
1088
|
+
const copy = new _TiledPatternObject(this.source, this.config, this.area);
|
|
1010
1089
|
copy.set({
|
|
1011
1090
|
left: this.left,
|
|
1012
1091
|
top: this.top,
|
|
1092
|
+
angle: this.angle,
|
|
1093
|
+
scaleX: this.scaleX,
|
|
1094
|
+
scaleY: this.scaleY,
|
|
1095
|
+
width: this.width,
|
|
1096
|
+
height: this.height,
|
|
1013
1097
|
visible: this.visible,
|
|
1014
1098
|
opacity: this.opacity
|
|
1015
1099
|
});
|
|
1016
1100
|
return Promise.resolve(copy);
|
|
1017
1101
|
}
|
|
1018
1102
|
/**
|
|
1019
|
-
* The
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1103
|
+
* The source's tile footprint: its bounding box, minus a stroke it reserves
|
|
1104
|
+
* room for but never paints.
|
|
1105
|
+
*
|
|
1106
|
+
* fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
|
|
1107
|
+
* colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
|
|
1108
|
+
* stroke. Stepping the grid by that box puts a transparent seam between every
|
|
1109
|
+
* pair of neighbours at zero spacing: the artwork is a pixel narrower than the
|
|
1110
|
+
* box it is stepped by.
|
|
1022
1111
|
*/
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1112
|
+
sourceBox() {
|
|
1113
|
+
const rect = this.source.getBoundingRect();
|
|
1114
|
+
if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
|
|
1115
|
+
const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
|
|
1116
|
+
const radians = (this.source.angle ?? 0) * Math.PI / 180;
|
|
1117
|
+
const cos = Math.abs(Math.cos(radians));
|
|
1118
|
+
const sin = Math.abs(Math.sin(radians));
|
|
1119
|
+
return {
|
|
1120
|
+
width: dims.x * cos + dims.y * sin,
|
|
1121
|
+
height: dims.x * sin + dims.y * cos
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
/** The source's on-canvas width, before the tile scale. */
|
|
1125
|
+
baseW() {
|
|
1126
|
+
return Math.max(1, this.sourceBox().width);
|
|
1127
|
+
}
|
|
1128
|
+
/** The source's on-canvas height, before the tile scale. */
|
|
1129
|
+
baseH() {
|
|
1130
|
+
return Math.max(1, this.sourceBox().height);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Size to paint the snapshot at, for a tile of `tileW × tileH`.
|
|
1134
|
+
*
|
|
1135
|
+
* The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
|
|
1136
|
+
* raster up to whole pixels and pads it further for a shadow. Painting it into
|
|
1137
|
+
* the tile step would squeeze the artwork inside that padding — every tile
|
|
1138
|
+
* shrinks by up to a pixel and the grid shows transparent seams at zero
|
|
1139
|
+
* spacing. So the bitmap is painted at its own footprint, scaled by the same
|
|
1140
|
+
* factor the tile is, and the step stays the source's box.
|
|
1141
|
+
*/
|
|
1142
|
+
drawSize(tileW, tileH, bleed = 0) {
|
|
1143
|
+
const snapshot = this.snapshotEl;
|
|
1144
|
+
if (!snapshot || this.snapshotScale <= 0) {
|
|
1145
|
+
return { width: tileW + bleed, height: tileH + bleed };
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
|
|
1149
|
+
height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
/** Tile size from the config alone, ignoring any in-flight gesture. */
|
|
1153
|
+
baseTile() {
|
|
1154
|
+
const box = this.sourceBox();
|
|
1155
|
+
const scale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1156
|
+
const floor = this.tileFloor();
|
|
1157
|
+
return {
|
|
1158
|
+
tileW: Math.max(floor, box.width * scale),
|
|
1159
|
+
tileH: Math.max(floor, box.height * scale)
|
|
1160
|
+
};
|
|
1027
1161
|
}
|
|
1028
|
-
/** Tile size
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1162
|
+
/** Tile size as drawn right now, including a live scale gesture. */
|
|
1163
|
+
liveTile() {
|
|
1164
|
+
const base = this.baseTile();
|
|
1165
|
+
const floor = this.tileFloor();
|
|
1031
1166
|
return {
|
|
1032
|
-
tileW: Math.max(floor,
|
|
1033
|
-
tileH: Math.max(floor,
|
|
1167
|
+
tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
|
|
1168
|
+
tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
|
|
1034
1169
|
};
|
|
1035
1170
|
}
|
|
1036
|
-
/**
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
return
|
|
1171
|
+
/** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
|
|
1172
|
+
tileFloor() {
|
|
1173
|
+
const { width, height } = this.area;
|
|
1174
|
+
return Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1175
|
+
}
|
|
1176
|
+
/** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
|
|
1177
|
+
shiftVector(tileW, tileH, angle) {
|
|
1178
|
+
const dx = tileW * (clampPercent(this.config.offsetX) / 100);
|
|
1179
|
+
const dy = tileH * (clampPercent(this.config.offsetY) / 100);
|
|
1180
|
+
const radians = angle * Math.PI / 180;
|
|
1181
|
+
const cos = Math.cos(radians);
|
|
1182
|
+
const sin = Math.sin(radians);
|
|
1183
|
+
return new Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1184
|
+
}
|
|
1185
|
+
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1186
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1187
|
+
return new Point(origin.x + shift.x, origin.y + shift.y);
|
|
1040
1188
|
}
|
|
1041
1189
|
/**
|
|
1042
|
-
* Snapshot the source at (at least) `scale`, reusing the cached one
|
|
1043
|
-
* still sharp enough and the source has not changed.
|
|
1190
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
1191
|
+
* is still sharp enough and the source has not changed.
|
|
1044
1192
|
*/
|
|
1045
1193
|
ensureSnapshot(scale) {
|
|
1046
1194
|
const wanted = this.clampScale(scale);
|
|
@@ -1071,6 +1219,16 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1071
1219
|
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1072
1220
|
}
|
|
1073
1221
|
};
|
|
1222
|
+
function paintsStroke(object) {
|
|
1223
|
+
if (!object.strokeWidth || object.strokeWidth <= 0) return false;
|
|
1224
|
+
const { stroke } = object;
|
|
1225
|
+
if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
|
|
1226
|
+
return stroke !== null && stroke !== void 0;
|
|
1227
|
+
}
|
|
1228
|
+
function clampPercent(value) {
|
|
1229
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1230
|
+
return Math.max(-100, Math.min(100, value));
|
|
1231
|
+
}
|
|
1074
1232
|
function contextScale(ctx) {
|
|
1075
1233
|
if (typeof ctx.getTransform !== "function") return 1;
|
|
1076
1234
|
try {
|
|
@@ -1082,13 +1240,15 @@ function contextScale(ctx) {
|
|
|
1082
1240
|
}
|
|
1083
1241
|
|
|
1084
1242
|
// src/pattern/pattern-manager.ts
|
|
1243
|
+
var MIN_TILE_SCALE = 10;
|
|
1244
|
+
var MAX_TILE_SCALE = 300;
|
|
1085
1245
|
var PatternManager = class {
|
|
1086
1246
|
constructor(canvas, layers, history, events) {
|
|
1087
1247
|
this.canvas = canvas;
|
|
1088
1248
|
this.layers = layers;
|
|
1089
1249
|
this.history = history;
|
|
1090
1250
|
this.events = events;
|
|
1091
|
-
this.canvas.on("object:modified", this.
|
|
1251
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
1092
1252
|
this.canvas.on("text:changed", this.onSourceModified);
|
|
1093
1253
|
this.events.on("layer:removed", this.onLayerRemoved);
|
|
1094
1254
|
}
|
|
@@ -1097,6 +1257,11 @@ var PatternManager = class {
|
|
|
1097
1257
|
history;
|
|
1098
1258
|
events;
|
|
1099
1259
|
proxies = /* @__PURE__ */ new Map();
|
|
1260
|
+
onObjectModified = (event) => {
|
|
1261
|
+
const target = event.target;
|
|
1262
|
+
if (target instanceof TiledPatternObject) this.commitGesture(target);
|
|
1263
|
+
else this.invalidateFor(target);
|
|
1264
|
+
};
|
|
1100
1265
|
onSourceModified = (event) => {
|
|
1101
1266
|
this.invalidateFor(event.target);
|
|
1102
1267
|
};
|
|
@@ -1138,7 +1303,11 @@ var PatternManager = class {
|
|
|
1138
1303
|
try {
|
|
1139
1304
|
this.detach(layerId);
|
|
1140
1305
|
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1141
|
-
layer.fabricObject.set({
|
|
1306
|
+
layer.fabricObject.set({
|
|
1307
|
+
opacity: layer.opacity,
|
|
1308
|
+
selectable: !layer.locked,
|
|
1309
|
+
evented: !layer.locked
|
|
1310
|
+
});
|
|
1142
1311
|
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
1143
1312
|
this.canvas.requestRenderAll();
|
|
1144
1313
|
this.history.save();
|
|
@@ -1170,12 +1339,8 @@ var PatternManager = class {
|
|
|
1170
1339
|
}
|
|
1171
1340
|
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1172
1341
|
syncArea() {
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1175
|
-
for (const proxy of this.proxies.values()) {
|
|
1176
|
-
proxy.setArea(width, height);
|
|
1177
|
-
proxy.invalidate();
|
|
1178
|
-
}
|
|
1342
|
+
const area = this.area();
|
|
1343
|
+
for (const proxy of this.proxies.values()) proxy.setArea(area);
|
|
1179
1344
|
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1180
1345
|
}
|
|
1181
1346
|
/** Drop a layer's cached source snapshot (its content changed). */
|
|
@@ -1183,6 +1348,7 @@ var PatternManager = class {
|
|
|
1183
1348
|
const proxy = this.proxies.get(layerId);
|
|
1184
1349
|
if (!proxy) return;
|
|
1185
1350
|
proxy.invalidate();
|
|
1351
|
+
proxy.syncBox();
|
|
1186
1352
|
this.canvas.requestRenderAll();
|
|
1187
1353
|
}
|
|
1188
1354
|
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
@@ -1192,7 +1358,7 @@ var PatternManager = class {
|
|
|
1192
1358
|
this.attach(layer, state.config);
|
|
1193
1359
|
}
|
|
1194
1360
|
dispose() {
|
|
1195
|
-
this.canvas.off("object:modified", this.
|
|
1361
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
1196
1362
|
this.canvas.off("text:changed", this.onSourceModified);
|
|
1197
1363
|
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1198
1364
|
this.clearProxies();
|
|
@@ -1203,17 +1369,38 @@ var PatternManager = class {
|
|
|
1203
1369
|
this.proxies.clear();
|
|
1204
1370
|
}
|
|
1205
1371
|
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 });
|
|
1372
|
+
const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
|
|
1373
|
+
const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
|
|
1374
|
+
layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
|
|
1213
1375
|
this.proxies.set(layer.id, proxy);
|
|
1214
1376
|
this.layers.setRenderProxy(layer.id, proxy);
|
|
1377
|
+
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1215
1378
|
this.canvas.requestRenderAll();
|
|
1216
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1382
|
+
* position becomes the grid origin (carried by the source), scale becomes the
|
|
1383
|
+
* tile scale, rotation becomes the pattern angle. Read the live transform
|
|
1384
|
+
* first — writing the config re-fits the box and destroys it.
|
|
1385
|
+
*/
|
|
1386
|
+
commitGesture(proxy) {
|
|
1387
|
+
const layer = this.layers.findByObject(proxy);
|
|
1388
|
+
const state = layer?.meta.pattern;
|
|
1389
|
+
if (!layer || !state) return;
|
|
1390
|
+
const origin = proxy.liveOrigin();
|
|
1391
|
+
const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
|
|
1392
|
+
const angle = normalizeAngle(proxy.liveAngle());
|
|
1393
|
+
layer.fabricObject.setPositionByOrigin(origin, "center", "center");
|
|
1394
|
+
layer.fabricObject.setCoords();
|
|
1395
|
+
const config = { ...state.config, scale, angle };
|
|
1396
|
+
this.layers.setMeta(layer.id, { pattern: { ...state, config } });
|
|
1397
|
+
proxy.setConfig(config);
|
|
1398
|
+
this.canvas.requestRenderAll();
|
|
1399
|
+
this.history.save();
|
|
1400
|
+
}
|
|
1401
|
+
area() {
|
|
1402
|
+
return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
|
|
1403
|
+
}
|
|
1217
1404
|
detach(layerId) {
|
|
1218
1405
|
const proxy = this.proxies.get(layerId);
|
|
1219
1406
|
if (!proxy) {
|
|
@@ -1230,6 +1417,15 @@ var PatternManager = class {
|
|
|
1230
1417
|
if (layer) this.invalidate(layer.id);
|
|
1231
1418
|
}
|
|
1232
1419
|
};
|
|
1420
|
+
function clamp2(value, min, max) {
|
|
1421
|
+
if (!Number.isFinite(value)) return min;
|
|
1422
|
+
return Math.max(min, Math.min(max, value));
|
|
1423
|
+
}
|
|
1424
|
+
function normalizeAngle(angle) {
|
|
1425
|
+
if (!Number.isFinite(angle)) return 0;
|
|
1426
|
+
const wrapped = (angle % 360 + 360) % 360;
|
|
1427
|
+
return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
|
|
1428
|
+
}
|
|
1233
1429
|
function isLegacyState(state) {
|
|
1234
1430
|
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1235
1431
|
}
|
|
@@ -2529,6 +2725,737 @@ var MaskController = class {
|
|
|
2529
2725
|
}
|
|
2530
2726
|
};
|
|
2531
2727
|
|
|
2728
|
+
// src/masks/manager.ts
|
|
2729
|
+
import { Group as Group5 } from "fabric";
|
|
2730
|
+
|
|
2731
|
+
// src/masks/compose.ts
|
|
2732
|
+
import { Group, Rect as Rect2 } from "fabric";
|
|
2733
|
+
var MODE_OPERATION = {
|
|
2734
|
+
add: "source-over",
|
|
2735
|
+
subtract: "destination-out",
|
|
2736
|
+
intersect: "destination-in"
|
|
2737
|
+
};
|
|
2738
|
+
function neutralize(child) {
|
|
2739
|
+
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
2740
|
+
}
|
|
2741
|
+
function baseRect(box) {
|
|
2742
|
+
return new Rect2({
|
|
2743
|
+
left: box.left,
|
|
2744
|
+
top: box.top,
|
|
2745
|
+
width: Math.max(1, box.width),
|
|
2746
|
+
height: Math.max(1, box.height),
|
|
2747
|
+
originX: "left",
|
|
2748
|
+
originY: "top",
|
|
2749
|
+
fill: "#000000",
|
|
2750
|
+
objectCaching: false
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
function composeMaskGroup(children, entries, options) {
|
|
2754
|
+
if (children.length === 0) return void 0;
|
|
2755
|
+
children.forEach((child, index) => {
|
|
2756
|
+
const entry = entries[index];
|
|
2757
|
+
child.set({ objectCaching: false });
|
|
2758
|
+
if (!entry || !entry.visible) {
|
|
2759
|
+
neutralize(child);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
child.set({
|
|
2763
|
+
opacity: entry.opacity,
|
|
2764
|
+
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
const first = entries.find((entry) => entry.visible);
|
|
2768
|
+
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
2769
|
+
return new Group(withBase, {
|
|
2770
|
+
absolutePositioned: options.absolute,
|
|
2771
|
+
// Cached, so the children's compositing operations resolve against each
|
|
2772
|
+
// other instead of against the page underneath the mask.
|
|
2773
|
+
objectCaching: true,
|
|
2774
|
+
subTargetCheck: false,
|
|
2775
|
+
interactive: false
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function needsAbsoluteSpace(entries) {
|
|
2779
|
+
return entries.some((entry) => !entry.linked);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/masks/edit.ts
|
|
2783
|
+
import { util as util4 } from "fabric";
|
|
2784
|
+
|
|
2785
|
+
// src/masks/space.ts
|
|
2786
|
+
import { util as util3 } from "fabric";
|
|
2787
|
+
function matrixOf(object) {
|
|
2788
|
+
return object.calcTransformMatrix();
|
|
2789
|
+
}
|
|
2790
|
+
function applyMatrix(object, matrix) {
|
|
2791
|
+
const decomposed = util3.qrDecompose(matrix);
|
|
2792
|
+
object.set({
|
|
2793
|
+
flipX: false,
|
|
2794
|
+
flipY: false,
|
|
2795
|
+
originX: "center",
|
|
2796
|
+
originY: "center",
|
|
2797
|
+
left: decomposed.translateX,
|
|
2798
|
+
top: decomposed.translateY,
|
|
2799
|
+
scaleX: decomposed.scaleX,
|
|
2800
|
+
scaleY: decomposed.scaleY,
|
|
2801
|
+
angle: decomposed.angle,
|
|
2802
|
+
skewX: decomposed.skewX,
|
|
2803
|
+
skewY: 0
|
|
2804
|
+
});
|
|
2805
|
+
object.setCoords();
|
|
2806
|
+
}
|
|
2807
|
+
function toCanvasSpace(object, host) {
|
|
2808
|
+
applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
2809
|
+
}
|
|
2810
|
+
function toHostSpace(object, host) {
|
|
2811
|
+
applyMatrix(
|
|
2812
|
+
object,
|
|
2813
|
+
util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object))
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
function relativeMatrix(object, host) {
|
|
2817
|
+
return util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object));
|
|
2818
|
+
}
|
|
2819
|
+
function applyRelativeMatrix(object, host, rel) {
|
|
2820
|
+
applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), rel));
|
|
2821
|
+
}
|
|
2822
|
+
function asObject(clip) {
|
|
2823
|
+
return clip;
|
|
2824
|
+
}
|
|
2825
|
+
function toMatrix(values) {
|
|
2826
|
+
if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
|
|
2827
|
+
return null;
|
|
2828
|
+
return [values[0], values[1], values[2], values[3], values[4], values[5]];
|
|
2829
|
+
}
|
|
2830
|
+
function fitToBox(object, box, zoom = 1) {
|
|
2831
|
+
const width = Math.max(1, box.width) * zoom;
|
|
2832
|
+
const height = Math.max(1, box.height) * zoom;
|
|
2833
|
+
object.set({
|
|
2834
|
+
originX: "center",
|
|
2835
|
+
originY: "center",
|
|
2836
|
+
angle: 0,
|
|
2837
|
+
skewX: 0,
|
|
2838
|
+
skewY: 0,
|
|
2839
|
+
left: box.left + box.width / 2,
|
|
2840
|
+
top: box.top + box.height / 2,
|
|
2841
|
+
scaleX: width / Math.max(1, object.width ?? 1),
|
|
2842
|
+
scaleY: height / Math.max(1, object.height ?? 1)
|
|
2843
|
+
});
|
|
2844
|
+
object.setCoords();
|
|
2845
|
+
}
|
|
2846
|
+
function unwrapGroup(group) {
|
|
2847
|
+
const children = group.removeAll();
|
|
2848
|
+
for (const child of children) child.setCoords();
|
|
2849
|
+
return children;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/masks/edit.ts
|
|
2853
|
+
var MaskEditController = class {
|
|
2854
|
+
constructor(canvas, onCommit) {
|
|
2855
|
+
this.canvas = canvas;
|
|
2856
|
+
this.onCommit = onCommit;
|
|
2857
|
+
}
|
|
2858
|
+
canvas;
|
|
2859
|
+
onCommit;
|
|
2860
|
+
handle = null;
|
|
2861
|
+
editing = null;
|
|
2862
|
+
child = null;
|
|
2863
|
+
group = null;
|
|
2864
|
+
active() {
|
|
2865
|
+
return this.editing;
|
|
2866
|
+
}
|
|
2867
|
+
/** Put a handle on the canvas for `child`, an object inside `group`. */
|
|
2868
|
+
async begin(edit, group, child) {
|
|
2869
|
+
this.end();
|
|
2870
|
+
const handle = await child.clone();
|
|
2871
|
+
applyMatrix(handle, matrixOf(child));
|
|
2872
|
+
handle.set({
|
|
2873
|
+
// Outline only: the mask's own fill would sit over the artwork it is
|
|
2874
|
+
// supposed to be revealing.
|
|
2875
|
+
fill: "rgba(0,0,0,0.001)",
|
|
2876
|
+
stroke: "#e0b055",
|
|
2877
|
+
strokeWidth: 2,
|
|
2878
|
+
strokeDashArray: [6, 4],
|
|
2879
|
+
strokeUniform: true,
|
|
2880
|
+
opacity: 1,
|
|
2881
|
+
selectable: true,
|
|
2882
|
+
evented: true,
|
|
2883
|
+
hasControls: true,
|
|
2884
|
+
hasBorders: true,
|
|
2885
|
+
objectCaching: false,
|
|
2886
|
+
excludeFromExport: true
|
|
2887
|
+
});
|
|
2888
|
+
this.handle = handle;
|
|
2889
|
+
this.editing = edit;
|
|
2890
|
+
this.child = child;
|
|
2891
|
+
this.group = group;
|
|
2892
|
+
this.canvas.add(handle);
|
|
2893
|
+
this.canvas.setActiveObject(handle);
|
|
2894
|
+
this.canvas.on("object:moving", this.onTransform);
|
|
2895
|
+
this.canvas.on("object:scaling", this.onTransform);
|
|
2896
|
+
this.canvas.on("object:rotating", this.onTransform);
|
|
2897
|
+
this.canvas.on("object:modified", this.onModified);
|
|
2898
|
+
this.canvas.requestRenderAll();
|
|
2899
|
+
return true;
|
|
2900
|
+
}
|
|
2901
|
+
/** Take the handle down. The geometry it wrote is already in the clip. */
|
|
2902
|
+
end() {
|
|
2903
|
+
if (!this.handle) return;
|
|
2904
|
+
this.canvas.off("object:moving", this.onTransform);
|
|
2905
|
+
this.canvas.off("object:scaling", this.onTransform);
|
|
2906
|
+
this.canvas.off("object:rotating", this.onTransform);
|
|
2907
|
+
this.canvas.off("object:modified", this.onModified);
|
|
2908
|
+
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
2909
|
+
this.canvas.remove(this.handle);
|
|
2910
|
+
this.handle = null;
|
|
2911
|
+
this.editing = null;
|
|
2912
|
+
this.child = null;
|
|
2913
|
+
this.group = null;
|
|
2914
|
+
this.canvas.requestRenderAll();
|
|
2915
|
+
}
|
|
2916
|
+
dispose() {
|
|
2917
|
+
this.end();
|
|
2918
|
+
}
|
|
2919
|
+
onTransform = (event) => {
|
|
2920
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
2921
|
+
this.write();
|
|
2922
|
+
};
|
|
2923
|
+
onModified = (event) => {
|
|
2924
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
2925
|
+
this.write();
|
|
2926
|
+
this.onCommit();
|
|
2927
|
+
};
|
|
2928
|
+
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
2929
|
+
write() {
|
|
2930
|
+
if (!this.handle || !this.child || !this.group) return;
|
|
2931
|
+
applyMatrix(
|
|
2932
|
+
this.child,
|
|
2933
|
+
util4.multiplyTransformMatrices(
|
|
2934
|
+
util4.invertTransform(matrixOf(this.group)),
|
|
2935
|
+
matrixOf(this.handle)
|
|
2936
|
+
)
|
|
2937
|
+
);
|
|
2938
|
+
this.group.dirty = true;
|
|
2939
|
+
this.group.set({ dirty: true });
|
|
2940
|
+
this.canvas.requestRenderAll();
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
|
|
2944
|
+
// src/masks/store.ts
|
|
2945
|
+
import { Group as Group4 } from "fabric";
|
|
2946
|
+
|
|
2947
|
+
// src/masks/host.ts
|
|
2948
|
+
import { Rect as Rect3 } from "fabric";
|
|
2949
|
+
function findCanvasHost(layers) {
|
|
2950
|
+
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
2951
|
+
}
|
|
2952
|
+
function createCanvasHost(canvas, layers) {
|
|
2953
|
+
const rect = new Rect3({
|
|
2954
|
+
left: 0,
|
|
2955
|
+
top: 0,
|
|
2956
|
+
width: canvas.getWidth(),
|
|
2957
|
+
height: canvas.getHeight(),
|
|
2958
|
+
originX: "left",
|
|
2959
|
+
originY: "top",
|
|
2960
|
+
fill: "#000000",
|
|
2961
|
+
globalCompositeOperation: "destination-in",
|
|
2962
|
+
// It is edited from the layer list, never on the canvas: a drag box on
|
|
2963
|
+
// something that cannot be dragged only reads as broken.
|
|
2964
|
+
selectable: false,
|
|
2965
|
+
evented: false,
|
|
2966
|
+
hasControls: false,
|
|
2967
|
+
hasBorders: false,
|
|
2968
|
+
objectCaching: false
|
|
2969
|
+
});
|
|
2970
|
+
const layer = layers.add("mask", rect, "Design mask");
|
|
2971
|
+
layer.meta.canvasMask = true;
|
|
2972
|
+
layers.setLocked(layer.id, true);
|
|
2973
|
+
return layer;
|
|
2974
|
+
}
|
|
2975
|
+
function pinCanvasHost(layers) {
|
|
2976
|
+
const all = layers.getAll();
|
|
2977
|
+
const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
|
|
2978
|
+
if (hostIndex === -1) return false;
|
|
2979
|
+
let lastContent = -1;
|
|
2980
|
+
all.forEach((layer, index) => {
|
|
2981
|
+
if (layer.type !== "mask") lastContent = index;
|
|
2982
|
+
});
|
|
2983
|
+
if (hostIndex >= lastContent) return false;
|
|
2984
|
+
layers.reorder(all[hostIndex].id, all.length - 1);
|
|
2985
|
+
return true;
|
|
2986
|
+
}
|
|
2987
|
+
function hostBoxOf(canvas, host, absolute) {
|
|
2988
|
+
if (!host) {
|
|
2989
|
+
return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
|
|
2990
|
+
}
|
|
2991
|
+
if (absolute) {
|
|
2992
|
+
host.setCoords();
|
|
2993
|
+
const rect = host.getBoundingRect();
|
|
2994
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
2995
|
+
}
|
|
2996
|
+
const width = Math.max(1, host.width ?? 1);
|
|
2997
|
+
const height = Math.max(1, host.height ?? 1);
|
|
2998
|
+
return { left: -width / 2, top: -height / 2, width, height };
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/masks/install.ts
|
|
3002
|
+
import { Group as Group3 } from "fabric";
|
|
3003
|
+
function convertSpace(host, sources, absolute) {
|
|
3004
|
+
const wasAbsolute = host.clipPath instanceof Group3 ? host.clipPath.absolutePositioned : absolute;
|
|
3005
|
+
if (absolute === wasAbsolute) return;
|
|
3006
|
+
for (const source of sources) {
|
|
3007
|
+
if (absolute) toCanvasSpace(source, host);
|
|
3008
|
+
else toHostSpace(source, host);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function withRelativeTransforms(entries, sources, host, absolute) {
|
|
3012
|
+
return entries.map((entry, index) => {
|
|
3013
|
+
const source = sources[index];
|
|
3014
|
+
if (absolute && entry.linked && source) {
|
|
3015
|
+
return { ...entry, rel: [...relativeMatrix(source, host)] };
|
|
3016
|
+
}
|
|
3017
|
+
if (!entry.rel) return entry;
|
|
3018
|
+
const rest = { ...entry };
|
|
3019
|
+
delete rest.rel;
|
|
3020
|
+
return rest;
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
function installClip(host, entries, sources, box, absolute) {
|
|
3024
|
+
host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
|
|
3025
|
+
host.dirty = true;
|
|
3026
|
+
host.setCoords();
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/masks/store.ts
|
|
3030
|
+
var CANVAS_MASK_TARGET = "canvas";
|
|
3031
|
+
var DEFAULT_ENTRY = {
|
|
3032
|
+
mode: "add",
|
|
3033
|
+
linked: true,
|
|
3034
|
+
visible: true,
|
|
3035
|
+
opacity: 1
|
|
3036
|
+
};
|
|
3037
|
+
var MaskStackStore = class {
|
|
3038
|
+
constructor(canvas, layers, history, events) {
|
|
3039
|
+
this.canvas = canvas;
|
|
3040
|
+
this.layers = layers;
|
|
3041
|
+
this.history = history;
|
|
3042
|
+
this.events = events;
|
|
3043
|
+
}
|
|
3044
|
+
canvas;
|
|
3045
|
+
layers;
|
|
3046
|
+
history;
|
|
3047
|
+
events;
|
|
3048
|
+
selected = null;
|
|
3049
|
+
pinning = false;
|
|
3050
|
+
list(target) {
|
|
3051
|
+
return this.hostLayer(target)?.meta.maskStack ?? [];
|
|
3052
|
+
}
|
|
3053
|
+
get(target, maskId) {
|
|
3054
|
+
return this.list(target).find((entry) => entry.id === maskId);
|
|
3055
|
+
}
|
|
3056
|
+
/** Every target that currently carries at least one mask. */
|
|
3057
|
+
targets() {
|
|
3058
|
+
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3059
|
+
}
|
|
3060
|
+
/** The box a mask is fitted to, in the space the stack is composed in. */
|
|
3061
|
+
hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
|
|
3062
|
+
return hostBoxOf(this.canvas, this.host(target), absolute);
|
|
3063
|
+
}
|
|
3064
|
+
/** The host layer of a target, optionally creating the design overlay. */
|
|
3065
|
+
hostLayer(target, create = false) {
|
|
3066
|
+
if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
|
|
3067
|
+
const existing = findCanvasHost(this.layers);
|
|
3068
|
+
if (existing || !create) return existing;
|
|
3069
|
+
return createCanvasHost(this.canvas, this.layers);
|
|
3070
|
+
}
|
|
3071
|
+
host(target, create = false) {
|
|
3072
|
+
return this.hostLayer(target, create)?.fabricObject ?? null;
|
|
3073
|
+
}
|
|
3074
|
+
/** Re-pin the design overlay, guarding the reorder that re-triggers this. */
|
|
3075
|
+
pin() {
|
|
3076
|
+
if (this.pinning) return;
|
|
3077
|
+
this.pinning = true;
|
|
3078
|
+
try {
|
|
3079
|
+
pinCanvasHost(this.layers);
|
|
3080
|
+
} finally {
|
|
3081
|
+
this.pinning = false;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Take the current geometry back out of the composed clip, entry-aligned.
|
|
3086
|
+
*
|
|
3087
|
+
* The entry list is the authority on how to read the clip: with no entries the
|
|
3088
|
+
* clip predates the stack (a mask preset, or a single `clipPath` an older host
|
|
3089
|
+
* installed) and is one mask whole — including when it happens to be a group,
|
|
3090
|
+
* which is why this cannot just unwrap anything group-shaped.
|
|
3091
|
+
*/
|
|
3092
|
+
unwrap(target, host) {
|
|
3093
|
+
const clip = host.clipPath;
|
|
3094
|
+
if (!clip) return [];
|
|
3095
|
+
const entries = this.list(target);
|
|
3096
|
+
if (entries.length === 0 || !(clip instanceof Group4)) return [asObject(clip)];
|
|
3097
|
+
const children = unwrapGroup(clip);
|
|
3098
|
+
const extra = children.length - entries.length;
|
|
3099
|
+
return extra > 0 ? children.slice(extra) : children;
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
3103
|
+
* this, the first `add()` on an already-masked layer would compose a clip it
|
|
3104
|
+
* has no entry for and silently throw that mask away.
|
|
3105
|
+
*/
|
|
3106
|
+
entriesFor(target, sources) {
|
|
3107
|
+
const existing = this.list(target);
|
|
3108
|
+
if (existing.length > 0 || sources.length !== 1) return [...existing];
|
|
3109
|
+
const layer = this.hostLayer(target);
|
|
3110
|
+
const preset = layer?.meta.maskPreset;
|
|
3111
|
+
if (layer) {
|
|
3112
|
+
delete layer.meta.maskPreset;
|
|
3113
|
+
}
|
|
3114
|
+
return [
|
|
3115
|
+
{
|
|
3116
|
+
...DEFAULT_ENTRY,
|
|
3117
|
+
id: generateId(),
|
|
3118
|
+
name: typeof preset === "string" ? preset : "Mask",
|
|
3119
|
+
linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
|
|
3120
|
+
}
|
|
3121
|
+
];
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Install a stack: convert geometry into the space the stack needs, compose the
|
|
3125
|
+
* clip, store the entries, and commit one history checkpoint for the lot.
|
|
3126
|
+
*/
|
|
3127
|
+
commit(target, host, entries, sources, save = true, forceAbsolute = false) {
|
|
3128
|
+
const layer = this.hostLayer(target);
|
|
3129
|
+
if (!layer) return;
|
|
3130
|
+
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
3131
|
+
convertSpace(host, sources, absolute);
|
|
3132
|
+
const next = withRelativeTransforms(entries, sources, host, absolute);
|
|
3133
|
+
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
3134
|
+
if (next.length === 0) {
|
|
3135
|
+
delete layer.meta.maskStack;
|
|
3136
|
+
if (layer.meta.canvasMask) this.layers.remove(layer.id);
|
|
3137
|
+
} else {
|
|
3138
|
+
layer.meta.maskStack = next;
|
|
3139
|
+
}
|
|
3140
|
+
this.canvas.requestRenderAll();
|
|
3141
|
+
this.events.emit("masks:changed", { target });
|
|
3142
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
3143
|
+
if (save) this.history.save();
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
|
|
3147
|
+
// src/masks/manager.ts
|
|
3148
|
+
var LayerMaskManager = class extends MaskStackStore {
|
|
3149
|
+
// Committing mid-drag would recompose the group the handle writes into and
|
|
3150
|
+
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
3151
|
+
edits = new MaskEditController(this.canvas, () => this.history.save());
|
|
3152
|
+
onLayersChanged = () => this.pin();
|
|
3153
|
+
// Selecting a layer means the user has moved on from the mask they had open;
|
|
3154
|
+
// leaving both selected would show mask controls for an unrelated layer.
|
|
3155
|
+
onLayerSelected = () => {
|
|
3156
|
+
if (this.selected) this.select(null, null);
|
|
3157
|
+
};
|
|
3158
|
+
onObjectModified = (event) => {
|
|
3159
|
+
const object = event.target;
|
|
3160
|
+
if (!object) return;
|
|
3161
|
+
const layer = this.layers.findByObject(object);
|
|
3162
|
+
if (layer) this.reflow(layer.id);
|
|
3163
|
+
};
|
|
3164
|
+
constructor(canvas, layers, history, events) {
|
|
3165
|
+
super(canvas, layers, history, events);
|
|
3166
|
+
this.events.on("layers:changed", this.onLayersChanged);
|
|
3167
|
+
this.events.on("layer:selected", this.onLayerSelected);
|
|
3168
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
3169
|
+
}
|
|
3170
|
+
dispose() {
|
|
3171
|
+
this.edits.dispose();
|
|
3172
|
+
this.events.off("layers:changed", this.onLayersChanged);
|
|
3173
|
+
this.events.off("layer:selected", this.onLayerSelected);
|
|
3174
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
3175
|
+
this.selected = null;
|
|
3176
|
+
}
|
|
3177
|
+
// ─── Geometry editing ────────────────────────────────
|
|
3178
|
+
/** The mask currently being dragged on the canvas, if any. */
|
|
3179
|
+
editing() {
|
|
3180
|
+
return this.edits.active();
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Put drag handles on one mask. The stack is composed in canvas space for the
|
|
3184
|
+
* duration — a linked mask would otherwise sit in the host's space, where the
|
|
3185
|
+
* handle's own canvas coordinates mean something else entirely. `endEdit`
|
|
3186
|
+
* returns it to whichever space its entries call for.
|
|
3187
|
+
*/
|
|
3188
|
+
async beginEdit(target, maskId) {
|
|
3189
|
+
const host = this.host(target);
|
|
3190
|
+
if (!host) return false;
|
|
3191
|
+
const entries = this.list(target);
|
|
3192
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3193
|
+
if (index === -1) return false;
|
|
3194
|
+
this.endEdit(false);
|
|
3195
|
+
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3196
|
+
const group = host.clipPath instanceof Group5 ? host.clipPath : null;
|
|
3197
|
+
if (!group) return false;
|
|
3198
|
+
const children = group.getObjects();
|
|
3199
|
+
const child = children[children.length - entries.length + index];
|
|
3200
|
+
if (!child) return false;
|
|
3201
|
+
this.select(target, maskId);
|
|
3202
|
+
return this.edits.begin({ target, maskId }, group, child);
|
|
3203
|
+
}
|
|
3204
|
+
/** Take the handles down and settle the stack back into its own space. */
|
|
3205
|
+
endEdit(save = true) {
|
|
3206
|
+
const editing = this.edits.active();
|
|
3207
|
+
this.edits.end();
|
|
3208
|
+
if (editing) this.rebuild(editing.target, save);
|
|
3209
|
+
}
|
|
3210
|
+
// ─── Selection ───────────────────────────────────────
|
|
3211
|
+
getSelected() {
|
|
3212
|
+
return this.selected;
|
|
3213
|
+
}
|
|
3214
|
+
select(target, maskId) {
|
|
3215
|
+
this.selected = target && maskId ? { target, maskId } : null;
|
|
3216
|
+
this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
|
|
3217
|
+
}
|
|
3218
|
+
// ─── Mutations ───────────────────────────────────────
|
|
3219
|
+
add(target, object, options = {}) {
|
|
3220
|
+
const host = this.host(target, true);
|
|
3221
|
+
if (!host) return null;
|
|
3222
|
+
const sources = this.unwrap(target, host);
|
|
3223
|
+
const entries = this.entriesFor(target, sources);
|
|
3224
|
+
const entry = {
|
|
3225
|
+
...DEFAULT_ENTRY,
|
|
3226
|
+
id: generateId(),
|
|
3227
|
+
name: options.name ?? "Mask",
|
|
3228
|
+
...options.mode ? { mode: options.mode } : {},
|
|
3229
|
+
...options.linked === void 0 ? {} : { linked: options.linked },
|
|
3230
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3231
|
+
};
|
|
3232
|
+
if (target === CANVAS_MASK_TARGET) entry.linked = false;
|
|
3233
|
+
if (options.fit !== false) {
|
|
3234
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3235
|
+
}
|
|
3236
|
+
sources.push(object);
|
|
3237
|
+
entries.push(entry);
|
|
3238
|
+
this.commit(target, host, entries, sources);
|
|
3239
|
+
return entry;
|
|
3240
|
+
}
|
|
3241
|
+
/** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
|
|
3242
|
+
replaceGeometry(target, maskId, object, options = {}) {
|
|
3243
|
+
const host = this.host(target);
|
|
3244
|
+
if (!host) return false;
|
|
3245
|
+
const entries = [...this.list(target)];
|
|
3246
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3247
|
+
if (index === -1) return false;
|
|
3248
|
+
const sources = this.unwrap(target, host);
|
|
3249
|
+
if (options.fit !== false) {
|
|
3250
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3251
|
+
}
|
|
3252
|
+
sources[index] = object;
|
|
3253
|
+
entries[index] = {
|
|
3254
|
+
...entries[index],
|
|
3255
|
+
...options.name ? { name: options.name } : {},
|
|
3256
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3257
|
+
};
|
|
3258
|
+
this.commit(target, host, entries, sources);
|
|
3259
|
+
return true;
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
|
|
3263
|
+
* geometry it already has. This is what a zoom control drives: re-picking the
|
|
3264
|
+
* mask would cost another render of a generated alpha map just to resize it.
|
|
3265
|
+
*/
|
|
3266
|
+
refit(target, maskId, options = {}) {
|
|
3267
|
+
const host = this.host(target);
|
|
3268
|
+
if (!host) return false;
|
|
3269
|
+
const entries = [...this.list(target)];
|
|
3270
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3271
|
+
if (index === -1) return false;
|
|
3272
|
+
const sources = this.unwrap(target, host);
|
|
3273
|
+
const source = sources[index];
|
|
3274
|
+
if (!source) return false;
|
|
3275
|
+
fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3276
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
3277
|
+
return true;
|
|
3278
|
+
}
|
|
3279
|
+
remove(target, maskId) {
|
|
3280
|
+
const host = this.host(target);
|
|
3281
|
+
if (!host) return false;
|
|
3282
|
+
const entries = [...this.list(target)];
|
|
3283
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3284
|
+
if (index === -1) return false;
|
|
3285
|
+
const sources = this.unwrap(target, host);
|
|
3286
|
+
entries.splice(index, 1);
|
|
3287
|
+
sources.splice(index, 1);
|
|
3288
|
+
if (this.selected?.maskId === maskId) this.select(null, null);
|
|
3289
|
+
this.commit(target, host, entries, sources);
|
|
3290
|
+
return true;
|
|
3291
|
+
}
|
|
3292
|
+
clear(target) {
|
|
3293
|
+
const host = this.host(target);
|
|
3294
|
+
if (!host) return false;
|
|
3295
|
+
if (this.list(target).length === 0) return false;
|
|
3296
|
+
if (this.selected?.target === target) this.select(null, null);
|
|
3297
|
+
this.commit(target, host, [], []);
|
|
3298
|
+
return true;
|
|
3299
|
+
}
|
|
3300
|
+
reorder(target, maskId, index) {
|
|
3301
|
+
const host = this.host(target);
|
|
3302
|
+
if (!host) return false;
|
|
3303
|
+
const entries = [...this.list(target)];
|
|
3304
|
+
const from = entries.findIndex((entry2) => entry2.id === maskId);
|
|
3305
|
+
if (from === -1) return false;
|
|
3306
|
+
const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
|
|
3307
|
+
if (from === to) return false;
|
|
3308
|
+
const sources = this.unwrap(target, host);
|
|
3309
|
+
const [entry] = entries.splice(from, 1);
|
|
3310
|
+
const [source] = sources.splice(from, 1);
|
|
3311
|
+
entries.splice(to, 0, entry);
|
|
3312
|
+
sources.splice(to, 0, source);
|
|
3313
|
+
this.commit(target, host, entries, sources);
|
|
3314
|
+
return true;
|
|
3315
|
+
}
|
|
3316
|
+
/** Replace a mask's host metadata (which preset or generator produced it). */
|
|
3317
|
+
setMeta(target, maskId, meta) {
|
|
3318
|
+
return this.patch(target, maskId, () => ({ meta }));
|
|
3319
|
+
}
|
|
3320
|
+
setMode(target, maskId, mode) {
|
|
3321
|
+
return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
|
|
3322
|
+
}
|
|
3323
|
+
setVisible(target, maskId, visible) {
|
|
3324
|
+
return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
|
|
3325
|
+
}
|
|
3326
|
+
setOpacity(target, maskId, opacity) {
|
|
3327
|
+
if (!Number.isFinite(opacity)) return false;
|
|
3328
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
3329
|
+
return this.patch(
|
|
3330
|
+
target,
|
|
3331
|
+
maskId,
|
|
3332
|
+
(entry) => entry.opacity === next ? null : { opacity: next }
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
setName(target, maskId, name) {
|
|
3336
|
+
const trimmed = name.trim();
|
|
3337
|
+
if (!trimmed) return false;
|
|
3338
|
+
return this.patch(
|
|
3339
|
+
target,
|
|
3340
|
+
maskId,
|
|
3341
|
+
(entry) => entry.name === trimmed ? null : { name: trimmed }
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Link or unlink one mask. Unlinking pins it where it currently appears;
|
|
3346
|
+
* re-linking records its position relative to the host so later host moves
|
|
3347
|
+
* carry it along.
|
|
3348
|
+
*/
|
|
3349
|
+
setLinked(target, maskId, linked) {
|
|
3350
|
+
if (target === CANVAS_MASK_TARGET) return false;
|
|
3351
|
+
const host = this.host(target);
|
|
3352
|
+
if (!host) return false;
|
|
3353
|
+
const entries = [...this.list(target)];
|
|
3354
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3355
|
+
if (index === -1 || entries[index].linked === linked) return false;
|
|
3356
|
+
const sources = this.unwrap(target, host);
|
|
3357
|
+
entries[index] = { ...entries[index], linked };
|
|
3358
|
+
this.commit(target, host, entries, sources);
|
|
3359
|
+
return true;
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Consume a layer, turning its artwork into a mask. Without an explicit target
|
|
3363
|
+
* it masks the layer directly beneath it, and the bottom layer masks the whole
|
|
3364
|
+
* design — there is nothing under it to clip.
|
|
3365
|
+
*/
|
|
3366
|
+
convertLayer(layerId, target) {
|
|
3367
|
+
const layer = this.layers.get(layerId);
|
|
3368
|
+
if (!layer || layer.meta.canvasMask) return null;
|
|
3369
|
+
const ordered = this.layers.getAll();
|
|
3370
|
+
const index = ordered.findIndex((candidate) => candidate.id === layerId);
|
|
3371
|
+
const below = index > 0 ? ordered[index - 1] : void 0;
|
|
3372
|
+
const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
|
|
3373
|
+
if (resolved === layerId) return null;
|
|
3374
|
+
const object = layer.fabricObject;
|
|
3375
|
+
if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
|
|
3376
|
+
this.history.beginTransaction();
|
|
3377
|
+
try {
|
|
3378
|
+
if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
|
|
3379
|
+
this.canvas.remove(object);
|
|
3380
|
+
this.layers.remove(layerId);
|
|
3381
|
+
const entry = this.add(resolved, object, { name: layer.name, fit: false });
|
|
3382
|
+
return entry ? { target: resolved, entry } : null;
|
|
3383
|
+
} finally {
|
|
3384
|
+
this.history.endTransaction();
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
// ─── Rebuild / restore ───────────────────────────────
|
|
3388
|
+
/** Recompose one stack's clip from the geometry it already holds. */
|
|
3389
|
+
rebuild(target, save = false) {
|
|
3390
|
+
const host = this.host(target);
|
|
3391
|
+
if (!host) return;
|
|
3392
|
+
const sources = this.unwrap(target, host);
|
|
3393
|
+
if (sources.length === 0) return;
|
|
3394
|
+
this.commit(target, host, this.entriesFor(target, sources), sources, save);
|
|
3395
|
+
}
|
|
3396
|
+
/** Re-derive linked masks after the host moved (canvas-space stacks only). */
|
|
3397
|
+
reflow(target) {
|
|
3398
|
+
const host = this.host(target);
|
|
3399
|
+
if (!host) return;
|
|
3400
|
+
const entries = this.list(target);
|
|
3401
|
+
if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
|
|
3402
|
+
if (!entries.some((entry) => entry.linked && entry.rel)) return;
|
|
3403
|
+
const sources = this.unwrap(target, host);
|
|
3404
|
+
entries.forEach((entry, index) => {
|
|
3405
|
+
const source = sources[index];
|
|
3406
|
+
const rel = toMatrix(entry.rel);
|
|
3407
|
+
if (!source || !entry.linked || !rel) return;
|
|
3408
|
+
applyRelativeMatrix(source, host, rel);
|
|
3409
|
+
});
|
|
3410
|
+
this.commit(target, host, [...entries], sources);
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Adopt a clip this manager did not install — a host's own single `clipPath` —
|
|
3414
|
+
* as a one-entry stack, so it shows up in the UI as a mask row before anything
|
|
3415
|
+
* is added to it. A mask preset is left alone unless asked for by name: it is
|
|
3416
|
+
* still owned by `MaskPresetManager` until a stack operation takes it over.
|
|
3417
|
+
*/
|
|
3418
|
+
adopt(target, name) {
|
|
3419
|
+
const layer = this.hostLayer(target);
|
|
3420
|
+
const clip = layer?.fabricObject.clipPath;
|
|
3421
|
+
if (!layer || !clip || layer.meta.pattern) return null;
|
|
3422
|
+
if ((layer.meta.maskStack ?? []).length > 0) return null;
|
|
3423
|
+
const entry = {
|
|
3424
|
+
...DEFAULT_ENTRY,
|
|
3425
|
+
id: generateId(),
|
|
3426
|
+
name: name ?? "Mask",
|
|
3427
|
+
linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
|
|
3428
|
+
};
|
|
3429
|
+
layer.meta.maskStack = [entry];
|
|
3430
|
+
delete layer.meta.maskPreset;
|
|
3431
|
+
this.rebuild(target);
|
|
3432
|
+
return entry;
|
|
3433
|
+
}
|
|
3434
|
+
/** After a state restore: re-pin the design overlay and recompose every stack. */
|
|
3435
|
+
refreshAll() {
|
|
3436
|
+
this.selected = null;
|
|
3437
|
+
this.pin();
|
|
3438
|
+
for (const layer of this.layers.getAll()) {
|
|
3439
|
+
if ((layer.meta.maskStack ?? []).length === 0) continue;
|
|
3440
|
+
this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
// ─── Internals ───────────────────────────────────────
|
|
3444
|
+
patch(target, maskId, change) {
|
|
3445
|
+
const host = this.host(target);
|
|
3446
|
+
if (!host) return false;
|
|
3447
|
+
const entries = [...this.list(target)];
|
|
3448
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3449
|
+
if (index === -1) return false;
|
|
3450
|
+
const patch = change(entries[index]);
|
|
3451
|
+
if (!patch) return false;
|
|
3452
|
+
const sources = this.unwrap(target, host);
|
|
3453
|
+
entries[index] = { ...entries[index], ...patch };
|
|
3454
|
+
this.commit(target, host, entries, sources);
|
|
3455
|
+
return true;
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
|
|
2532
3459
|
// src/editor.ts
|
|
2533
3460
|
var MIN_ZOOM = 0.1;
|
|
2534
3461
|
var MAX_ZOOM = 8;
|
|
@@ -2546,6 +3473,8 @@ var CanvasEditor = class {
|
|
|
2546
3473
|
patterns;
|
|
2547
3474
|
curves;
|
|
2548
3475
|
maskPresets;
|
|
3476
|
+
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
3477
|
+
layerMasks;
|
|
2549
3478
|
fonts;
|
|
2550
3479
|
licensing;
|
|
2551
3480
|
pages;
|
|
@@ -2594,6 +3523,7 @@ var CanvasEditor = class {
|
|
|
2594
3523
|
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2595
3524
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2596
3525
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
3526
|
+
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
2597
3527
|
this.setupCanvasEvents();
|
|
2598
3528
|
this.refreshSelectionStyle();
|
|
2599
3529
|
this.history.saveImmediate();
|
|
@@ -2609,6 +3539,7 @@ var CanvasEditor = class {
|
|
|
2609
3539
|
{ originX: "left", originY: "top", ...options }
|
|
2610
3540
|
);
|
|
2611
3541
|
const layer = this.layers.add("image", img);
|
|
3542
|
+
this.layers.select(layer.id);
|
|
2612
3543
|
this.history.save();
|
|
2613
3544
|
return layer;
|
|
2614
3545
|
} catch (error) {
|
|
@@ -2668,12 +3599,14 @@ var CanvasEditor = class {
|
|
|
2668
3599
|
...options
|
|
2669
3600
|
});
|
|
2670
3601
|
const layer = this.layers.add("text", textbox);
|
|
3602
|
+
this.layers.select(layer.id);
|
|
2671
3603
|
this.history.save();
|
|
2672
3604
|
return layer;
|
|
2673
3605
|
}
|
|
2674
3606
|
addShape(plugin, options) {
|
|
2675
3607
|
const obj = plugin.create(options);
|
|
2676
3608
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
3609
|
+
this.layers.select(layer.id);
|
|
2677
3610
|
this.history.save();
|
|
2678
3611
|
return layer;
|
|
2679
3612
|
}
|
|
@@ -2699,7 +3632,7 @@ var CanvasEditor = class {
|
|
|
2699
3632
|
const { objects, options } = await loadSVGFromString(resolved);
|
|
2700
3633
|
const validObjects = objects.filter((object) => object !== null);
|
|
2701
3634
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2702
|
-
const group =
|
|
3635
|
+
const group = util5.groupSVGElements(validObjects, options);
|
|
2703
3636
|
group.set({
|
|
2704
3637
|
left: this.canvas.getWidth() / 2,
|
|
2705
3638
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2707,6 +3640,7 @@ var CanvasEditor = class {
|
|
|
2707
3640
|
originY: "center"
|
|
2708
3641
|
});
|
|
2709
3642
|
const layer = this.layers.add("template", group, template.name);
|
|
3643
|
+
this.layers.select(layer.id);
|
|
2710
3644
|
this.history.save();
|
|
2711
3645
|
return layer;
|
|
2712
3646
|
}
|
|
@@ -2758,7 +3692,7 @@ var CanvasEditor = class {
|
|
|
2758
3692
|
opacity: layer.opacity
|
|
2759
3693
|
});
|
|
2760
3694
|
this.patterns.attachTo(copy);
|
|
2761
|
-
this.
|
|
3695
|
+
this.layers.select(copy.id);
|
|
2762
3696
|
this.canvas.requestRenderAll();
|
|
2763
3697
|
this.history.save();
|
|
2764
3698
|
return copy;
|
|
@@ -2791,7 +3725,7 @@ var CanvasEditor = class {
|
|
|
2791
3725
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2792
3726
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2793
3727
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2794
|
-
const group = new
|
|
3728
|
+
const group = new Group6(objects);
|
|
2795
3729
|
const grouped = this.layers.add("group", group, name);
|
|
2796
3730
|
grouped.meta.groupChildren = childData;
|
|
2797
3731
|
this.layers.select(grouped.id);
|
|
@@ -2806,11 +3740,9 @@ var CanvasEditor = class {
|
|
|
2806
3740
|
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
2807
3741
|
const group = grouped.fabricObject;
|
|
2808
3742
|
return this.history.transaction(() => {
|
|
2809
|
-
const transform = group.calcTransformMatrix();
|
|
2810
3743
|
const objects = group.removeAll();
|
|
2811
3744
|
this.layers.remove(id);
|
|
2812
3745
|
const restored = objects.map((object, index) => {
|
|
2813
|
-
util3.addTransformToObject(object, transform);
|
|
2814
3746
|
object.setCoords();
|
|
2815
3747
|
const data = childData[index];
|
|
2816
3748
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -2838,6 +3770,7 @@ var CanvasEditor = class {
|
|
|
2838
3770
|
try {
|
|
2839
3771
|
await deserializeEditor(this, state);
|
|
2840
3772
|
await this.patterns.rehydrateAll();
|
|
3773
|
+
this.layerMasks.refreshAll();
|
|
2841
3774
|
} catch (error) {
|
|
2842
3775
|
if (!managedByHistory) {
|
|
2843
3776
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3410,6 +4343,7 @@ var CanvasEditor = class {
|
|
|
3410
4343
|
// ─── Cleanup ────────────────────────────────────────
|
|
3411
4344
|
dispose() {
|
|
3412
4345
|
this.masks.dispose();
|
|
4346
|
+
this.layerMasks.dispose();
|
|
3413
4347
|
this.snapping.dispose();
|
|
3414
4348
|
this.crop.dispose();
|
|
3415
4349
|
this.history.dispose();
|
|
@@ -3532,6 +4466,7 @@ var AnnotationOverlay = class {
|
|
|
3532
4466
|
};
|
|
3533
4467
|
export {
|
|
3534
4468
|
AnnotationOverlay,
|
|
4469
|
+
CANVAS_MASK_TARGET,
|
|
3535
4470
|
CANVAS_SIZE_PRESETS,
|
|
3536
4471
|
CanvasEditor,
|
|
3537
4472
|
CropController,
|
|
@@ -3544,6 +4479,7 @@ export {
|
|
|
3544
4479
|
HistoryManager,
|
|
3545
4480
|
Layer,
|
|
3546
4481
|
LayerManager,
|
|
4482
|
+
LayerMaskManager,
|
|
3547
4483
|
LicenseManager,
|
|
3548
4484
|
MaskController,
|
|
3549
4485
|
MaskPresetManager,
|
|
@@ -3565,6 +4501,7 @@ export {
|
|
|
3565
4501
|
buildCurvePathData,
|
|
3566
4502
|
clamp,
|
|
3567
4503
|
clearTextureMaskCache,
|
|
4504
|
+
composeMaskGroup,
|
|
3568
4505
|
computeCoverPlacement,
|
|
3569
4506
|
computePrintAreaClip,
|
|
3570
4507
|
computeTilePositions,
|
|
@@ -3577,11 +4514,13 @@ export {
|
|
|
3577
4514
|
exportPNG,
|
|
3578
4515
|
exportPrintArea,
|
|
3579
4516
|
exportSVG,
|
|
4517
|
+
fitToBox,
|
|
3580
4518
|
generateId,
|
|
3581
4519
|
isCssColor,
|
|
3582
4520
|
isMaskPresetId,
|
|
3583
4521
|
isShapeMaskId,
|
|
3584
4522
|
isTextureMaskId,
|
|
4523
|
+
needsAbsoluteSpace,
|
|
3585
4524
|
readLayerShadow,
|
|
3586
4525
|
renderTextureMask,
|
|
3587
4526
|
resetTransform,
|
|
@@ -3589,6 +4528,9 @@ export {
|
|
|
3589
4528
|
round2,
|
|
3590
4529
|
sanitizeSvg,
|
|
3591
4530
|
serializeEditor,
|
|
3592
|
-
shapeMaskPathData
|
|
4531
|
+
shapeMaskPathData,
|
|
4532
|
+
toCanvasSpace,
|
|
4533
|
+
toHostSpace,
|
|
4534
|
+
unwrapGroup
|
|
3593
4535
|
};
|
|
3594
4536
|
//# sourceMappingURL=index.mjs.map
|