@overtone-art/canvas-editor-core 0.3.1 → 0.4.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 +177 -46
- package/dist/index.d.ts +177 -46
- package/dist/index.global.js +53 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +535 -332
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +509 -305
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-Dh1unrzT.d.mts → types-BNzE_X5M.d.mts} +16 -22
- package/dist/{types-Dh1unrzT.d.ts → types-BNzE_X5M.d.ts} +16 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
CropController: () => CropController,
|
|
27
27
|
DEFAULT_LAYER_SHADOW: () => DEFAULT_LAYER_SHADOW,
|
|
28
28
|
DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
|
|
29
|
+
DEFAULT_SELECTION_STYLE: () => DEFAULT_SELECTION_STYLE,
|
|
29
30
|
DEFAULT_TEXT_CURVE: () => DEFAULT_TEXT_CURVE,
|
|
30
31
|
EventEmitter: () => EventEmitter,
|
|
31
32
|
FontRegistry: () => FontRegistry,
|
|
@@ -44,15 +45,14 @@ __export(index_exports, {
|
|
|
44
45
|
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
45
46
|
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
46
47
|
TextCurveManager: () => TextCurveManager,
|
|
48
|
+
TiledPatternObject: () => TiledPatternObject,
|
|
47
49
|
UnitConverter: () => UnitConverter,
|
|
48
50
|
applyAspectLock: () => applyAspectLock,
|
|
49
51
|
applyLayerShadow: () => applyLayerShadow,
|
|
50
|
-
|
|
52
|
+
applyObjectSelectionStyle: () => applyObjectSelectionStyle,
|
|
53
|
+
applySelectionStyle: () => applySelectionStyle,
|
|
51
54
|
buildCurvePathData: () => buildCurvePathData,
|
|
52
|
-
buildPatternDataURL: () => buildPatternDataURL,
|
|
53
|
-
captureLocks: () => captureLocks,
|
|
54
55
|
clamp: () => clamp,
|
|
55
|
-
clearPatternImageCache: () => clearPatternImageCache,
|
|
56
56
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
57
57
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
58
58
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
@@ -71,7 +71,6 @@ __export(index_exports, {
|
|
|
71
71
|
isMaskPresetId: () => isMaskPresetId,
|
|
72
72
|
isShapeMaskId: () => isShapeMaskId,
|
|
73
73
|
isTextureMaskId: () => isTextureMaskId,
|
|
74
|
-
loadPatternImage: () => loadPatternImage,
|
|
75
74
|
readLayerShadow: () => readLayerShadow,
|
|
76
75
|
renderTextureMask: () => renderTextureMask,
|
|
77
76
|
resetTransform: () => resetTransform,
|
|
@@ -84,7 +83,7 @@ __export(index_exports, {
|
|
|
84
83
|
module.exports = __toCommonJS(index_exports);
|
|
85
84
|
|
|
86
85
|
// src/editor.ts
|
|
87
|
-
var
|
|
86
|
+
var import_fabric10 = require("fabric");
|
|
88
87
|
|
|
89
88
|
// src/events.ts
|
|
90
89
|
var EventEmitter = class {
|
|
@@ -136,6 +135,12 @@ var Layer = class {
|
|
|
136
135
|
/** Non-fabric data (e.g. pattern config) that must persist with the layer. */
|
|
137
136
|
meta;
|
|
138
137
|
fabricObject;
|
|
138
|
+
/**
|
|
139
|
+
* Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
|
|
140
|
+
* part of the canvas but never part of the document: it is not serialized, and
|
|
141
|
+
* the layer's real object stays the one every editor API talks to.
|
|
142
|
+
*/
|
|
143
|
+
renderProxy;
|
|
139
144
|
constructor(type, fabricObject, name, id) {
|
|
140
145
|
this.id = id ?? generateId();
|
|
141
146
|
this.type = type;
|
|
@@ -145,6 +150,7 @@ var Layer = class {
|
|
|
145
150
|
this.opacity = 1;
|
|
146
151
|
this.meta = {};
|
|
147
152
|
this.fabricObject = fabricObject;
|
|
153
|
+
this.renderProxy = null;
|
|
148
154
|
this.fabricObject._layerId = this.id;
|
|
149
155
|
}
|
|
150
156
|
hasMeta() {
|
|
@@ -193,6 +199,7 @@ var LayerManager = class {
|
|
|
193
199
|
if (index === -1) return false;
|
|
194
200
|
const layer = this.layers[index];
|
|
195
201
|
this.canvas.remove(layer.fabricObject);
|
|
202
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
196
203
|
this.layers.splice(index, 1);
|
|
197
204
|
this.events.emit("layer:removed", { layerId: id });
|
|
198
205
|
this.emitChanged();
|
|
@@ -216,6 +223,10 @@ var LayerManager = class {
|
|
|
216
223
|
evented: !layer.locked
|
|
217
224
|
});
|
|
218
225
|
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
226
|
+
if (layer.renderProxy) {
|
|
227
|
+
fabricObject.set({ opacity: 0 });
|
|
228
|
+
this.syncZOrder();
|
|
229
|
+
}
|
|
219
230
|
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
220
231
|
this.canvas.requestRenderAll();
|
|
221
232
|
this.events.emit("layer:modified", { layerId: id });
|
|
@@ -223,6 +234,36 @@ var LayerManager = class {
|
|
|
223
234
|
this.onPropertyChanged?.();
|
|
224
235
|
return true;
|
|
225
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Attach (or clear) the object drawn in place of a layer's own object. The
|
|
239
|
+
* proxy tracks the layer's stacking position, visibility and opacity, and is
|
|
240
|
+
* removed with the layer — it must never outlive or drift from its source.
|
|
241
|
+
*/
|
|
242
|
+
setRenderProxy(id, proxy) {
|
|
243
|
+
const layer = this.get(id);
|
|
244
|
+
if (!layer || layer.renderProxy === proxy) return;
|
|
245
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
246
|
+
layer.renderProxy = proxy;
|
|
247
|
+
if (proxy) {
|
|
248
|
+
proxy.set({ visible: layer.visible, opacity: layer.opacity });
|
|
249
|
+
this.canvas.add(proxy);
|
|
250
|
+
this.syncZOrder();
|
|
251
|
+
}
|
|
252
|
+
this.canvas.requestRenderAll();
|
|
253
|
+
this.emitChanged();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Re-stack every canvas object to match layer order, keeping each proxy
|
|
257
|
+
* directly above the source it stands in for. Canvas indices can't be derived
|
|
258
|
+
* from layer indices once proxies are in the array, so the order is rebuilt
|
|
259
|
+
* front-to-back instead of computed.
|
|
260
|
+
*/
|
|
261
|
+
syncZOrder() {
|
|
262
|
+
for (const layer of this.layers) {
|
|
263
|
+
this.canvas.bringObjectToFront(layer.fabricObject);
|
|
264
|
+
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
226
267
|
reorder(id, newIndex) {
|
|
227
268
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
228
269
|
if (oldIndex === -1) return false;
|
|
@@ -231,9 +272,7 @@ var LayerManager = class {
|
|
|
231
272
|
if (oldIndex === clamped) return false;
|
|
232
273
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
233
274
|
this.layers.splice(clamped, 0, layer);
|
|
234
|
-
this.
|
|
235
|
-
this.canvas.moveObjectTo(l.fabricObject, i);
|
|
236
|
-
});
|
|
275
|
+
this.syncZOrder();
|
|
237
276
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
238
277
|
this.emitChanged();
|
|
239
278
|
this.onPropertyChanged?.();
|
|
@@ -270,6 +309,7 @@ var LayerManager = class {
|
|
|
270
309
|
if (layer.visible === visible) return;
|
|
271
310
|
layer.visible = visible;
|
|
272
311
|
layer.fabricObject.visible = visible;
|
|
312
|
+
if (layer.renderProxy) layer.renderProxy.visible = visible;
|
|
273
313
|
this.canvas.requestRenderAll();
|
|
274
314
|
this.emitChanged();
|
|
275
315
|
this.onPropertyChanged?.();
|
|
@@ -291,7 +331,8 @@ var LayerManager = class {
|
|
|
291
331
|
const next = Math.max(0, Math.min(1, opacity));
|
|
292
332
|
if (layer.opacity === next) return;
|
|
293
333
|
layer.opacity = next;
|
|
294
|
-
layer.
|
|
334
|
+
if (layer.renderProxy) layer.renderProxy.opacity = next;
|
|
335
|
+
else layer.fabricObject.opacity = next;
|
|
295
336
|
this.canvas.requestRenderAll();
|
|
296
337
|
this.emitChanged();
|
|
297
338
|
this.onPropertyChanged?.();
|
|
@@ -339,6 +380,7 @@ var LayerManager = class {
|
|
|
339
380
|
clear() {
|
|
340
381
|
for (const layer of this.layers) {
|
|
341
382
|
this.canvas.remove(layer.fabricObject);
|
|
383
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
342
384
|
}
|
|
343
385
|
this.layers = [];
|
|
344
386
|
this.emitChanged();
|
|
@@ -872,199 +914,415 @@ var CropController = class {
|
|
|
872
914
|
};
|
|
873
915
|
var STROKE2 = "#22c55e";
|
|
874
916
|
|
|
875
|
-
// src/pattern.ts
|
|
917
|
+
// src/pattern/pattern-manager.ts
|
|
918
|
+
var import_fabric3 = require("fabric");
|
|
919
|
+
|
|
920
|
+
// src/pattern/tiled-pattern-object.ts
|
|
876
921
|
var import_fabric2 = require("fabric");
|
|
922
|
+
|
|
923
|
+
// src/pattern/tile-geometry.ts
|
|
877
924
|
var MAX_TILES_PER_AXIS = 200;
|
|
925
|
+
function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
926
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
927
|
+
const radius = cornerRadius(anchor, targetW, targetH);
|
|
928
|
+
const span = radius * 2;
|
|
929
|
+
const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
|
|
930
|
+
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
931
|
+
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
932
|
+
const cols = Math.ceil(span / tileW) + 2;
|
|
933
|
+
const rows = Math.ceil(span / tileH) + 2;
|
|
934
|
+
const halfCols = Math.ceil(cols / 2);
|
|
935
|
+
const halfRows = Math.ceil(rows / 2);
|
|
936
|
+
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
937
|
+
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
938
|
+
const placements = [];
|
|
939
|
+
for (let j = -halfRows; j <= halfRows; j++) {
|
|
940
|
+
for (let i = -halfCols; i <= halfCols; i++) {
|
|
941
|
+
let x = i * tileW + shiftX;
|
|
942
|
+
let y = j * tileH + shiftY;
|
|
943
|
+
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
944
|
+
x += tileW * (config.horizontalOffset / 100);
|
|
945
|
+
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
946
|
+
y += tileH * (config.horizontalOffset / 100);
|
|
947
|
+
}
|
|
948
|
+
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
949
|
+
placements.push({ x, y, rotation });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return placements;
|
|
953
|
+
}
|
|
954
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
955
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
956
|
+
ctx.save();
|
|
957
|
+
ctx.translate(anchor.x, anchor.y);
|
|
958
|
+
ctx.rotate(config.angle * Math.PI / 180);
|
|
959
|
+
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
|
|
960
|
+
ctx.save();
|
|
961
|
+
ctx.translate(tile.x, tile.y);
|
|
962
|
+
ctx.rotate(tile.rotation);
|
|
963
|
+
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
964
|
+
ctx.restore();
|
|
965
|
+
}
|
|
966
|
+
ctx.restore();
|
|
967
|
+
}
|
|
968
|
+
function cornerRadius(origin, w, h) {
|
|
969
|
+
const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
|
|
970
|
+
const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
|
|
971
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
972
|
+
}
|
|
973
|
+
function clampOffset(value) {
|
|
974
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
975
|
+
return Math.max(-100, Math.min(100, value));
|
|
976
|
+
}
|
|
977
|
+
function mod2(n) {
|
|
978
|
+
return (n % 2 + 2) % 2;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/pattern/tiled-pattern-object.ts
|
|
982
|
+
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
983
|
+
var MAX_SNAPSHOT_SCALE = 8;
|
|
984
|
+
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
985
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
|
|
986
|
+
static type = "TiledPattern";
|
|
987
|
+
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
988
|
+
source;
|
|
989
|
+
config;
|
|
990
|
+
snapshotEl = null;
|
|
991
|
+
snapshotScale = 0;
|
|
992
|
+
constructor(source, config, width, height) {
|
|
993
|
+
super({
|
|
994
|
+
// Explicit origin: fabric v7 defaults to `center`, and a centre-origin
|
|
995
|
+
// object placed at 0,0 covers a quarter of the canvas. That exact bug is
|
|
996
|
+
// why the old bake-into-the-layer pattern landed in the top-left corner.
|
|
997
|
+
originX: "left",
|
|
998
|
+
originY: "top",
|
|
999
|
+
left: 0,
|
|
1000
|
+
top: 0,
|
|
1001
|
+
width,
|
|
1002
|
+
height,
|
|
1003
|
+
// Pointer events belong to the source underneath, and fabric's object
|
|
1004
|
+
// cache would freeze the tiling at screen resolution.
|
|
1005
|
+
selectable: false,
|
|
1006
|
+
evented: false,
|
|
1007
|
+
objectCaching: false,
|
|
1008
|
+
hasControls: false,
|
|
1009
|
+
hasBorders: false
|
|
1010
|
+
});
|
|
1011
|
+
this.source = source;
|
|
1012
|
+
this.config = config;
|
|
1013
|
+
}
|
|
1014
|
+
/** Swap in a new config; the next render picks it up. */
|
|
1015
|
+
setConfig(config) {
|
|
1016
|
+
this.config = config;
|
|
1017
|
+
this.dirty = true;
|
|
1018
|
+
}
|
|
1019
|
+
/** Resize to a new print area. */
|
|
1020
|
+
setArea(width, height) {
|
|
1021
|
+
this.set({ width, height });
|
|
1022
|
+
this.setCoords();
|
|
1023
|
+
}
|
|
1024
|
+
/** Drop the cached source snapshot (e.g. the source was edited). */
|
|
1025
|
+
invalidate() {
|
|
1026
|
+
this.snapshotEl = null;
|
|
1027
|
+
this.snapshotScale = 0;
|
|
1028
|
+
this.dirty = true;
|
|
1029
|
+
}
|
|
1030
|
+
/** Free the offscreen snapshot. */
|
|
1031
|
+
dispose() {
|
|
1032
|
+
this.snapshotEl = null;
|
|
1033
|
+
this.snapshotScale = 0;
|
|
1034
|
+
}
|
|
1035
|
+
_render(ctx) {
|
|
1036
|
+
const width = this.width ?? 0;
|
|
1037
|
+
const height = this.height ?? 0;
|
|
1038
|
+
if (width <= 0 || height <= 0) return;
|
|
1039
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1040
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * tileScale);
|
|
1041
|
+
if (!snapshot) return;
|
|
1042
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
1043
|
+
ctx.save();
|
|
1044
|
+
ctx.translate(-width / 2, -height / 2);
|
|
1045
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
1046
|
+
ctx.restore();
|
|
1047
|
+
}
|
|
1048
|
+
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
1049
|
+
_toSVG() {
|
|
1050
|
+
const width = this.width ?? 0;
|
|
1051
|
+
const height = this.height ?? 0;
|
|
1052
|
+
if (width <= 0 || height <= 0) return [];
|
|
1053
|
+
const el = document.createElement("canvas");
|
|
1054
|
+
el.width = Math.max(1, Math.round(width));
|
|
1055
|
+
el.height = Math.max(1, Math.round(height));
|
|
1056
|
+
const ctx = el.getContext("2d");
|
|
1057
|
+
if (!ctx) return [];
|
|
1058
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1059
|
+
const snapshot = this.ensureSnapshot(tileScale);
|
|
1060
|
+
if (!snapshot) return [];
|
|
1061
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
1062
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
1063
|
+
return [
|
|
1064
|
+
`<image x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" `,
|
|
1065
|
+
`xlink:href="${el.toDataURL("image/png")}"></image>
|
|
1066
|
+
`
|
|
1067
|
+
];
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
1071
|
+
* registry, which cannot carry a live source reference. Export paths clone
|
|
1072
|
+
* every canvas object, so without this a print export would silently lose the
|
|
1073
|
+
* tiling. The copy shares the source (it only ever reads from it).
|
|
1074
|
+
*/
|
|
1075
|
+
clone() {
|
|
1076
|
+
const copy = new _TiledPatternObject(
|
|
1077
|
+
this.source,
|
|
1078
|
+
this.config,
|
|
1079
|
+
this.width ?? 0,
|
|
1080
|
+
this.height ?? 0
|
|
1081
|
+
);
|
|
1082
|
+
copy.set({
|
|
1083
|
+
left: this.left,
|
|
1084
|
+
top: this.top,
|
|
1085
|
+
visible: this.visible,
|
|
1086
|
+
opacity: this.opacity
|
|
1087
|
+
});
|
|
1088
|
+
return Promise.resolve(copy);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1092
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1093
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1094
|
+
*/
|
|
1095
|
+
toObject() {
|
|
1096
|
+
const plain = super.toObject();
|
|
1097
|
+
delete plain.source;
|
|
1098
|
+
return plain;
|
|
1099
|
+
}
|
|
1100
|
+
/** Tile size in canvas units, floored so a tiny tile can't spawn a huge loop. */
|
|
1101
|
+
tileSize(snapshot, tileScale, width, height) {
|
|
1102
|
+
const floor = Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1103
|
+
return {
|
|
1104
|
+
tileW: Math.max(floor, snapshot.width / this.snapshotScale * tileScale),
|
|
1105
|
+
tileH: Math.max(floor, snapshot.height / this.snapshotScale * tileScale)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/** Grid anchor: the source's centre, in this object's coordinates. */
|
|
1109
|
+
gridOrigin() {
|
|
1110
|
+
const centre = this.source.getCenterPoint();
|
|
1111
|
+
return { x: centre.x - (this.left ?? 0), y: centre.y - (this.top ?? 0) };
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one when it is
|
|
1115
|
+
* still sharp enough and the source has not changed.
|
|
1116
|
+
*/
|
|
1117
|
+
ensureSnapshot(scale) {
|
|
1118
|
+
const wanted = this.clampScale(scale);
|
|
1119
|
+
const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
|
|
1120
|
+
if (!stale) return this.snapshotEl;
|
|
1121
|
+
const source = this.source;
|
|
1122
|
+
const opacity = source.opacity;
|
|
1123
|
+
source.opacity = 1;
|
|
1124
|
+
try {
|
|
1125
|
+
const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
|
|
1126
|
+
if (!el.width || !el.height) return null;
|
|
1127
|
+
this.snapshotEl = el;
|
|
1128
|
+
this.snapshotScale = wanted;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return this.snapshotEl;
|
|
1131
|
+
} finally {
|
|
1132
|
+
source.opacity = opacity;
|
|
1133
|
+
source.dirty = false;
|
|
1134
|
+
}
|
|
1135
|
+
return this.snapshotEl;
|
|
1136
|
+
}
|
|
1137
|
+
/** Bound the snapshot by both a linear scale and a total pixel budget. */
|
|
1138
|
+
clampScale(scale) {
|
|
1139
|
+
const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
|
|
1140
|
+
const rect = this.source.getBoundingRect();
|
|
1141
|
+
const area = Math.max(1, rect.width * rect.height);
|
|
1142
|
+
const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
|
|
1143
|
+
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
function contextScale(ctx) {
|
|
1147
|
+
if (typeof ctx.getTransform !== "function") return 1;
|
|
1148
|
+
try {
|
|
1149
|
+
const t = ctx.getTransform();
|
|
1150
|
+
return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return 1;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/pattern/pattern-manager.ts
|
|
878
1157
|
var PatternManager = class {
|
|
879
|
-
constructor(canvas, layers, history, events
|
|
1158
|
+
constructor(canvas, layers, history, events) {
|
|
880
1159
|
this.canvas = canvas;
|
|
881
1160
|
this.layers = layers;
|
|
882
1161
|
this.history = history;
|
|
883
1162
|
this.events = events;
|
|
884
|
-
this.
|
|
1163
|
+
this.canvas.on("object:modified", this.onSourceModified);
|
|
1164
|
+
this.canvas.on("text:changed", this.onSourceModified);
|
|
1165
|
+
this.events.on("layer:removed", this.onLayerRemoved);
|
|
885
1166
|
}
|
|
886
1167
|
canvas;
|
|
887
1168
|
layers;
|
|
888
1169
|
history;
|
|
889
1170
|
events;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1171
|
+
proxies = /* @__PURE__ */ new Map();
|
|
1172
|
+
onSourceModified = (event) => {
|
|
1173
|
+
this.invalidateFor(event.target);
|
|
1174
|
+
};
|
|
1175
|
+
onLayerRemoved = ({ layerId }) => {
|
|
1176
|
+
const proxy = this.proxies.get(layerId);
|
|
1177
|
+
if (!proxy) return;
|
|
1178
|
+
proxy.dispose();
|
|
1179
|
+
this.proxies.delete(layerId);
|
|
1180
|
+
};
|
|
896
1181
|
isPattern(layerId) {
|
|
897
1182
|
return !!this.layers.get(layerId)?.meta.pattern;
|
|
898
1183
|
}
|
|
899
1184
|
getConfig(layerId) {
|
|
900
1185
|
return this.layers.get(layerId)?.meta.pattern?.config ?? null;
|
|
901
1186
|
}
|
|
902
|
-
/** Turn a
|
|
903
|
-
apply(layerId, config) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
if (
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
config,
|
|
913
|
-
originalSrc: elementToDataURL(image) ?? image.getSrc(),
|
|
914
|
-
originalClip: clip ? clip.toObject() : null,
|
|
915
|
-
originalLocks: captureLocks(image),
|
|
916
|
-
original: {
|
|
917
|
-
left: image.left ?? 0,
|
|
918
|
-
top: image.top ?? 0,
|
|
919
|
-
scaleX: image.scaleX ?? 1,
|
|
920
|
-
scaleY: image.scaleY ?? 1,
|
|
921
|
-
width: image.width ?? 0,
|
|
922
|
-
height: image.height ?? 0,
|
|
923
|
-
angle: image.angle ?? 0,
|
|
924
|
-
cropX: image.cropX ?? 0,
|
|
925
|
-
cropY: image.cropY ?? 0
|
|
926
|
-
}
|
|
927
|
-
};
|
|
1187
|
+
/** Turn a layer into a repeating pattern, or update an existing one. */
|
|
1188
|
+
async apply(layerId, config) {
|
|
1189
|
+
const layer = this.layers.get(layerId);
|
|
1190
|
+
if (!layer) return;
|
|
1191
|
+
try {
|
|
1192
|
+
this.layers.setMeta(layerId, { pattern: { config } });
|
|
1193
|
+
const proxy = this.proxies.get(layerId);
|
|
1194
|
+
if (proxy) {
|
|
1195
|
+
proxy.setConfig(config);
|
|
1196
|
+
this.canvas.requestRenderAll();
|
|
928
1197
|
} else {
|
|
929
|
-
layer
|
|
930
|
-
}
|
|
931
|
-
try {
|
|
932
|
-
await this.renderLayer(layer);
|
|
933
|
-
} catch (err) {
|
|
934
|
-
if (firstEnable) delete layer.meta.pattern;
|
|
935
|
-
throw err;
|
|
1198
|
+
this.attach(layer, config);
|
|
936
1199
|
}
|
|
937
1200
|
this.history.save();
|
|
938
|
-
}
|
|
939
|
-
this.events.emit("error", { message: "Failed to apply
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
this.events.emit("error", { message: "Failed to apply pattern", error });
|
|
940
1203
|
throw error;
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* Stretch every restored pattern layer back over the full print area and
|
|
945
|
-
* re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
|
|
946
|
-
* it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
|
|
947
|
-
* after a state restore, where the canvas may be a different display size
|
|
948
|
-
* than when the pattern was baked — and to repair states saved while a
|
|
949
|
-
* pattern could still be dragged out of the print area.
|
|
950
|
-
*/
|
|
951
|
-
repinAll() {
|
|
952
|
-
const cw = this.canvas.getWidth();
|
|
953
|
-
const ch = this.canvas.getHeight();
|
|
954
|
-
let changed = false;
|
|
955
|
-
for (const layer of this.layers.getAll()) {
|
|
956
|
-
if (!layer.meta.pattern || layer.type !== "image") continue;
|
|
957
|
-
const image = layer.fabricObject;
|
|
958
|
-
image.set({
|
|
959
|
-
left: 0,
|
|
960
|
-
top: 0,
|
|
961
|
-
angle: 0,
|
|
962
|
-
scaleX: cw / (image.width || cw),
|
|
963
|
-
scaleY: ch / (image.height || ch)
|
|
964
|
-
});
|
|
965
|
-
applyPatternLocks(image);
|
|
966
|
-
image.setCoords();
|
|
967
|
-
changed = true;
|
|
968
1204
|
}
|
|
969
|
-
if (changed) this.canvas.requestRenderAll();
|
|
970
1205
|
}
|
|
971
|
-
/**
|
|
972
|
-
disable(layerId) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
left: state.original.left,
|
|
981
|
-
top: state.original.top,
|
|
982
|
-
scaleX: state.original.scaleX,
|
|
983
|
-
scaleY: state.original.scaleY,
|
|
984
|
-
width: state.original.width,
|
|
985
|
-
height: state.original.height,
|
|
986
|
-
cropX: state.original.cropX,
|
|
987
|
-
cropY: state.original.cropY,
|
|
988
|
-
angle: state.original.angle
|
|
989
|
-
});
|
|
990
|
-
image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
991
|
-
restoreLocks(image, state.originalLocks);
|
|
992
|
-
image.setCoords();
|
|
993
|
-
delete layer.meta.pattern;
|
|
1206
|
+
/** Drop the tiling and show the source again. */
|
|
1207
|
+
async disable(layerId) {
|
|
1208
|
+
const layer = this.layers.get(layerId);
|
|
1209
|
+
if (!layer?.meta.pattern) return;
|
|
1210
|
+
try {
|
|
1211
|
+
this.detach(layerId);
|
|
1212
|
+
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1213
|
+
layer.fabricObject.set({ opacity: layer.opacity });
|
|
1214
|
+
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
994
1215
|
this.canvas.requestRenderAll();
|
|
995
1216
|
this.history.save();
|
|
996
|
-
}
|
|
997
|
-
this.events.emit("error", { message: "Failed to clear
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
this.events.emit("error", { message: "Failed to clear pattern", error });
|
|
998
1219
|
throw error;
|
|
999
|
-
}
|
|
1220
|
+
}
|
|
1000
1221
|
}
|
|
1001
|
-
/**
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* Rebuild every proxy after a state restore, migrating any layer that was
|
|
1224
|
+
* saved by the old bake-into-the-layer engine.
|
|
1225
|
+
*/
|
|
1226
|
+
async rehydrateAll() {
|
|
1227
|
+
this.clearProxies();
|
|
1228
|
+
for (const layer of this.layers.getAll()) {
|
|
1229
|
+
const state = layer.meta.pattern;
|
|
1230
|
+
if (!state) continue;
|
|
1231
|
+
try {
|
|
1232
|
+
if (isLegacyState(state)) {
|
|
1233
|
+
await unbakeLegacyLayer(layer, state);
|
|
1234
|
+
this.layers.setMeta(layer.id, { pattern: { config: state.config } });
|
|
1235
|
+
}
|
|
1236
|
+
this.attach(layer, state.config);
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
this.events.emit("error", { message: "Failed to restore pattern layer", error });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
this.canvas.requestRenderAll();
|
|
1010
1242
|
}
|
|
1011
|
-
|
|
1243
|
+
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1244
|
+
syncArea() {
|
|
1245
|
+
const width = this.canvas.getWidth();
|
|
1246
|
+
const height = this.canvas.getHeight();
|
|
1247
|
+
for (const proxy of this.proxies.values()) {
|
|
1248
|
+
proxy.setArea(width, height);
|
|
1249
|
+
proxy.invalidate();
|
|
1250
|
+
}
|
|
1251
|
+
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1252
|
+
}
|
|
1253
|
+
/** Drop a layer's cached source snapshot (its content changed). */
|
|
1254
|
+
invalidate(layerId) {
|
|
1255
|
+
const proxy = this.proxies.get(layerId);
|
|
1256
|
+
if (!proxy) return;
|
|
1257
|
+
proxy.invalidate();
|
|
1258
|
+
this.canvas.requestRenderAll();
|
|
1259
|
+
}
|
|
1260
|
+
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
1261
|
+
attachTo(layer) {
|
|
1012
1262
|
const state = layer.meta.pattern;
|
|
1013
|
-
if (!state) return;
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1263
|
+
if (!state || this.proxies.has(layer.id)) return;
|
|
1264
|
+
this.attach(layer, state.config);
|
|
1265
|
+
}
|
|
1266
|
+
dispose() {
|
|
1267
|
+
this.canvas.off("object:modified", this.onSourceModified);
|
|
1268
|
+
this.canvas.off("text:changed", this.onSourceModified);
|
|
1269
|
+
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1270
|
+
this.clearProxies();
|
|
1271
|
+
}
|
|
1272
|
+
/** Release every proxy and the offscreen snapshot it holds. */
|
|
1273
|
+
clearProxies() {
|
|
1274
|
+
for (const proxy of this.proxies.values()) proxy.dispose();
|
|
1275
|
+
this.proxies.clear();
|
|
1276
|
+
}
|
|
1277
|
+
attach(layer, config) {
|
|
1278
|
+
const proxy = new TiledPatternObject(
|
|
1279
|
+
layer.fabricObject,
|
|
1280
|
+
config,
|
|
1281
|
+
this.canvas.getWidth(),
|
|
1282
|
+
this.canvas.getHeight()
|
|
1030
1283
|
);
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
top: 0,
|
|
1035
|
-
scaleX: 1,
|
|
1036
|
-
scaleY: 1,
|
|
1037
|
-
width: cw,
|
|
1038
|
-
height: ch,
|
|
1039
|
-
cropX: 0,
|
|
1040
|
-
cropY: 0,
|
|
1041
|
-
angle: 0
|
|
1042
|
-
});
|
|
1043
|
-
image.clipPath = void 0;
|
|
1044
|
-
applyPatternLocks(image);
|
|
1045
|
-
image.setCoords();
|
|
1284
|
+
layer.fabricObject.set({ opacity: 0 });
|
|
1285
|
+
this.proxies.set(layer.id, proxy);
|
|
1286
|
+
this.layers.setRenderProxy(layer.id, proxy);
|
|
1046
1287
|
this.canvas.requestRenderAll();
|
|
1047
1288
|
}
|
|
1289
|
+
detach(layerId) {
|
|
1290
|
+
const proxy = this.proxies.get(layerId);
|
|
1291
|
+
if (!proxy) {
|
|
1292
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1296
|
+
proxy.dispose();
|
|
1297
|
+
this.proxies.delete(layerId);
|
|
1298
|
+
}
|
|
1299
|
+
invalidateFor(target) {
|
|
1300
|
+
if (!target) return;
|
|
1301
|
+
const layer = this.layers.findByObject(target);
|
|
1302
|
+
if (layer) this.invalidate(layer.id);
|
|
1303
|
+
}
|
|
1048
1304
|
};
|
|
1049
|
-
function
|
|
1050
|
-
return
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
hasControls: false
|
|
1305
|
+
function isLegacyState(state) {
|
|
1306
|
+
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1307
|
+
}
|
|
1308
|
+
async function unbakeLegacyLayer(layer, state) {
|
|
1309
|
+
const image = layer.fabricObject;
|
|
1310
|
+
if (typeof image.setSrc !== "function" || !state.original) return;
|
|
1311
|
+
await image.setSrc(state.originalSrc);
|
|
1312
|
+
image.set({
|
|
1313
|
+
left: state.original.left,
|
|
1314
|
+
top: state.original.top,
|
|
1315
|
+
scaleX: state.original.scaleX,
|
|
1316
|
+
scaleY: state.original.scaleY,
|
|
1317
|
+
width: state.original.width,
|
|
1318
|
+
height: state.original.height,
|
|
1319
|
+
cropX: state.original.cropX,
|
|
1320
|
+
cropY: state.original.cropY,
|
|
1321
|
+
angle: state.original.angle
|
|
1067
1322
|
});
|
|
1323
|
+
image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1324
|
+
restoreLocks(image, state.originalLocks);
|
|
1325
|
+
image.setCoords();
|
|
1068
1326
|
}
|
|
1069
1327
|
function restoreLocks(obj, locks) {
|
|
1070
1328
|
obj.set(
|
|
@@ -1078,124 +1336,9 @@ function restoreLocks(obj, locks) {
|
|
|
1078
1336
|
}
|
|
1079
1337
|
);
|
|
1080
1338
|
}
|
|
1081
|
-
function elementToDataURL(image) {
|
|
1082
|
-
try {
|
|
1083
|
-
const el = image.getElement();
|
|
1084
|
-
const w = el.naturalWidth || el.width;
|
|
1085
|
-
const h = el.naturalHeight || el.height;
|
|
1086
|
-
if (!w || !h) return null;
|
|
1087
|
-
const off = document.createElement("canvas");
|
|
1088
|
-
off.width = w;
|
|
1089
|
-
off.height = h;
|
|
1090
|
-
const ctx = off.getContext("2d");
|
|
1091
|
-
if (!ctx) return null;
|
|
1092
|
-
ctx.drawImage(el, 0, 0);
|
|
1093
|
-
return off.toDataURL("image/png");
|
|
1094
|
-
} catch {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
var IMAGE_CACHE_MAX = 16;
|
|
1099
|
-
var imageCache = /* @__PURE__ */ new Map();
|
|
1100
|
-
function loadPatternImage(src, resolver) {
|
|
1101
|
-
const cached = imageCache.get(src);
|
|
1102
|
-
if (cached) {
|
|
1103
|
-
imageCache.delete(src);
|
|
1104
|
-
imageCache.set(src, cached);
|
|
1105
|
-
return cached;
|
|
1106
|
-
}
|
|
1107
|
-
const promise = decodeImage(src).catch(async (originalError) => {
|
|
1108
|
-
if (!resolver) throw originalError;
|
|
1109
|
-
const resolved = await resolver(src);
|
|
1110
|
-
if (!resolved || resolved === src) {
|
|
1111
|
-
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
1112
|
-
cause: originalError
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
return decodeImage(resolved);
|
|
1116
|
-
});
|
|
1117
|
-
promise.catch(() => {
|
|
1118
|
-
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
1119
|
-
});
|
|
1120
|
-
imageCache.set(src, promise);
|
|
1121
|
-
if (imageCache.size > IMAGE_CACHE_MAX) {
|
|
1122
|
-
const oldest = imageCache.keys().next().value;
|
|
1123
|
-
if (oldest !== void 0) imageCache.delete(oldest);
|
|
1124
|
-
}
|
|
1125
|
-
return promise;
|
|
1126
|
-
}
|
|
1127
|
-
function decodeImage(src) {
|
|
1128
|
-
return new Promise((resolve, reject) => {
|
|
1129
|
-
const img = new Image();
|
|
1130
|
-
img.crossOrigin = "anonymous";
|
|
1131
|
-
img.onload = () => resolve(img);
|
|
1132
|
-
img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
|
|
1133
|
-
img.src = src;
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
function clearPatternImageCache() {
|
|
1137
|
-
imageCache.clear();
|
|
1138
|
-
}
|
|
1139
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
1140
|
-
const img = await loadPatternImage(src, sourceResolver);
|
|
1141
|
-
const off = document.createElement("canvas");
|
|
1142
|
-
off.width = Math.max(1, Math.round(targetW));
|
|
1143
|
-
off.height = Math.max(1, Math.round(targetH));
|
|
1144
|
-
const ctx = off.getContext("2d");
|
|
1145
|
-
if (!ctx) return off.toDataURL("image/png");
|
|
1146
|
-
drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
|
|
1147
|
-
return off.toDataURL("image/png");
|
|
1148
|
-
}
|
|
1149
|
-
function computeTilePositions(config, targetW, targetH, baseW, baseH) {
|
|
1150
|
-
const diag = Math.sqrt(targetW * targetW + targetH * targetH);
|
|
1151
|
-
const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
|
|
1152
|
-
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
1153
|
-
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
1154
|
-
const cols = Math.ceil(diag / tileW) + 2;
|
|
1155
|
-
const rows = Math.ceil(diag / tileH) + 2;
|
|
1156
|
-
const halfCols = Math.ceil(cols / 2);
|
|
1157
|
-
const halfRows = Math.ceil(rows / 2);
|
|
1158
|
-
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
1159
|
-
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
1160
|
-
const placements = [];
|
|
1161
|
-
for (let j = -halfRows; j <= halfRows; j++) {
|
|
1162
|
-
for (let i = -halfCols; i <= halfCols; i++) {
|
|
1163
|
-
let x = i * tileW + shiftX;
|
|
1164
|
-
let y = j * tileH + shiftY;
|
|
1165
|
-
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
1166
|
-
x += tileW * (config.horizontalOffset / 100);
|
|
1167
|
-
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
1168
|
-
y += tileH * (config.horizontalOffset / 100);
|
|
1169
|
-
}
|
|
1170
|
-
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
1171
|
-
placements.push({ x, y, rotation });
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
return placements;
|
|
1175
|
-
}
|
|
1176
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
|
|
1177
|
-
ctx.save();
|
|
1178
|
-
ctx.translate(targetW / 2, targetH / 2);
|
|
1179
|
-
ctx.rotate(config.angle * Math.PI / 180);
|
|
1180
|
-
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
|
|
1181
|
-
ctx.save();
|
|
1182
|
-
ctx.translate(tile.x, tile.y);
|
|
1183
|
-
ctx.rotate(tile.rotation);
|
|
1184
|
-
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
1185
|
-
ctx.restore();
|
|
1186
|
-
}
|
|
1187
|
-
ctx.restore();
|
|
1188
|
-
}
|
|
1189
|
-
function clampOffset(value) {
|
|
1190
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1191
|
-
return Math.max(-100, Math.min(100, value));
|
|
1192
|
-
}
|
|
1193
|
-
function mod2(n) {
|
|
1194
|
-
return (n % 2 + 2) % 2;
|
|
1195
|
-
}
|
|
1196
1339
|
|
|
1197
1340
|
// src/text-curve.ts
|
|
1198
|
-
var
|
|
1341
|
+
var import_fabric4 = require("fabric");
|
|
1199
1342
|
var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
|
|
1200
1343
|
var MIN_ARC = 0.5;
|
|
1201
1344
|
var FULL_CIRCLE_ARC = 99.5;
|
|
@@ -1318,7 +1461,7 @@ var TextCurveManager = class {
|
|
|
1318
1461
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1319
1462
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1320
1463
|
text.set({
|
|
1321
|
-
path: new
|
|
1464
|
+
path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
|
|
1322
1465
|
pathAlign: "center",
|
|
1323
1466
|
pathSide: "left",
|
|
1324
1467
|
pathStartOffset: Math.max(0, (curve.length - run) / 2)
|
|
@@ -1360,7 +1503,7 @@ var TextCurveManager = class {
|
|
|
1360
1503
|
};
|
|
1361
1504
|
|
|
1362
1505
|
// src/mask-presets/manager.ts
|
|
1363
|
-
var
|
|
1506
|
+
var import_fabric5 = require("fabric");
|
|
1364
1507
|
|
|
1365
1508
|
// src/mask-presets/shapes.ts
|
|
1366
1509
|
var SHAPE_MASK_IDS = [
|
|
@@ -1627,13 +1770,13 @@ var MaskPresetManager = class {
|
|
|
1627
1770
|
objectCaching: false
|
|
1628
1771
|
};
|
|
1629
1772
|
if (isShapeMaskId(id)) {
|
|
1630
|
-
return new
|
|
1773
|
+
return new import_fabric5.Path(shapeMaskPathData(id), {
|
|
1631
1774
|
...shared,
|
|
1632
1775
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1633
1776
|
scaleY: height / SHAPE_MASK_BOX
|
|
1634
1777
|
});
|
|
1635
1778
|
}
|
|
1636
|
-
return new
|
|
1779
|
+
return new import_fabric5.FabricImage(renderTextureMask(id), {
|
|
1637
1780
|
...shared,
|
|
1638
1781
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1639
1782
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1642,7 +1785,7 @@ var MaskPresetManager = class {
|
|
|
1642
1785
|
};
|
|
1643
1786
|
|
|
1644
1787
|
// src/shadow.ts
|
|
1645
|
-
var
|
|
1788
|
+
var import_fabric6 = require("fabric");
|
|
1646
1789
|
|
|
1647
1790
|
// src/utils/color.ts
|
|
1648
1791
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -1680,7 +1823,7 @@ function applyLayerShadow(object, config) {
|
|
|
1680
1823
|
}
|
|
1681
1824
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1682
1825
|
object.set({
|
|
1683
|
-
shadow: new
|
|
1826
|
+
shadow: new import_fabric6.Shadow({
|
|
1684
1827
|
color,
|
|
1685
1828
|
blur: Math.max(0, next.blur),
|
|
1686
1829
|
offsetX: next.offsetX,
|
|
@@ -1689,6 +1832,43 @@ function applyLayerShadow(object, config) {
|
|
|
1689
1832
|
});
|
|
1690
1833
|
}
|
|
1691
1834
|
|
|
1835
|
+
// src/selection-style.ts
|
|
1836
|
+
var DEFAULT_SELECTION_STYLE = {
|
|
1837
|
+
borderColor: "#c9a96e",
|
|
1838
|
+
cornerColor: "#c9a96e",
|
|
1839
|
+
cornerStrokeColor: "#101010",
|
|
1840
|
+
cornerSize: 11,
|
|
1841
|
+
cornerStyle: "circle",
|
|
1842
|
+
borderWidth: 1,
|
|
1843
|
+
borderDashArray: null,
|
|
1844
|
+
padding: 0,
|
|
1845
|
+
marqueeFill: "rgba(201, 169, 110, 0.12)"
|
|
1846
|
+
};
|
|
1847
|
+
function applyObjectSelectionStyle(object, style, zoom) {
|
|
1848
|
+
const scale = zoom > 0 ? 1 / zoom : 1;
|
|
1849
|
+
object.set({
|
|
1850
|
+
cornerSize: style.cornerSize * scale,
|
|
1851
|
+
cornerStyle: style.cornerStyle,
|
|
1852
|
+
cornerColor: style.cornerColor,
|
|
1853
|
+
cornerStrokeColor: style.cornerStrokeColor,
|
|
1854
|
+
// A filled handle is what makes the stroke colour visible at all.
|
|
1855
|
+
transparentCorners: false,
|
|
1856
|
+
borderColor: style.borderColor,
|
|
1857
|
+
borderScaleFactor: style.borderWidth * scale,
|
|
1858
|
+
borderDashArray: style.borderDashArray?.map((segment) => segment * scale) ?? null,
|
|
1859
|
+
padding: style.padding * scale
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
function applySelectionStyle(canvas, style, zoom) {
|
|
1863
|
+
for (const object of canvas.getObjects()) applyObjectSelectionStyle(object, style, zoom);
|
|
1864
|
+
const active = canvas.getActiveObject();
|
|
1865
|
+
if (active) applyObjectSelectionStyle(active, style, zoom);
|
|
1866
|
+
canvas.selectionColor = style.marqueeFill;
|
|
1867
|
+
canvas.selectionBorderColor = style.borderColor;
|
|
1868
|
+
canvas.selectionLineWidth = style.borderWidth * (zoom > 0 ? 1 / zoom : 1);
|
|
1869
|
+
canvas.requestRenderAll();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1692
1872
|
// src/transform.ts
|
|
1693
1873
|
var SIDE_CONTROLS = ["ml", "mr", "mt", "mb"];
|
|
1694
1874
|
function applyAspectLock(object, locked) {
|
|
@@ -1757,7 +1937,7 @@ var UnitConverter = class {
|
|
|
1757
1937
|
};
|
|
1758
1938
|
|
|
1759
1939
|
// src/serialization.ts
|
|
1760
|
-
var
|
|
1940
|
+
var import_fabric7 = require("fabric");
|
|
1761
1941
|
var VERSION = "2.0.0";
|
|
1762
1942
|
function serializeEditor(editor) {
|
|
1763
1943
|
return {
|
|
@@ -1793,7 +1973,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1793
1973
|
}
|
|
1794
1974
|
const staged = await Promise.all(
|
|
1795
1975
|
state.layers.map(async (serialized) => {
|
|
1796
|
-
const fabricObject = (await
|
|
1976
|
+
const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1797
1977
|
if (!fabricObject) {
|
|
1798
1978
|
const source = serialized.fabricObject.src;
|
|
1799
1979
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1804,7 +1984,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1804
1984
|
return { serialized, fabricObject };
|
|
1805
1985
|
})
|
|
1806
1986
|
);
|
|
1807
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
1987
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1808
1988
|
if (state.backgroundImage && !stagedBackground) {
|
|
1809
1989
|
const source = state.backgroundImage.src;
|
|
1810
1990
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1851,7 +2031,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1851
2031
|
}
|
|
1852
2032
|
|
|
1853
2033
|
// src/export.ts
|
|
1854
|
-
var
|
|
2034
|
+
var import_fabric8 = require("fabric");
|
|
1855
2035
|
|
|
1856
2036
|
// src/displacement.ts
|
|
1857
2037
|
var CHANNEL_INDEX = {
|
|
@@ -1945,7 +2125,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1945
2125
|
}
|
|
1946
2126
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1947
2127
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1948
|
-
const canvas = new
|
|
2128
|
+
const canvas = new import_fabric8.StaticCanvas(element, {
|
|
1949
2129
|
width: options.width ?? source.getWidth(),
|
|
1950
2130
|
height: options.height ?? source.getHeight(),
|
|
1951
2131
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -1975,7 +2155,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
1975
2155
|
throw new Error("Print area does not overlap the canvas");
|
|
1976
2156
|
}
|
|
1977
2157
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1978
|
-
const canvas = new
|
|
2158
|
+
const canvas = new import_fabric8.StaticCanvas(element, { width, height });
|
|
1979
2159
|
try {
|
|
1980
2160
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
1981
2161
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2415,7 +2595,7 @@ var ProjectManager = class {
|
|
|
2415
2595
|
};
|
|
2416
2596
|
|
|
2417
2597
|
// src/mask.ts
|
|
2418
|
-
var
|
|
2598
|
+
var import_fabric9 = require("fabric");
|
|
2419
2599
|
var MaskRefinementError = class extends Error {
|
|
2420
2600
|
constructor(code, message, cause) {
|
|
2421
2601
|
super(message);
|
|
@@ -2447,7 +2627,7 @@ var MaskController = class {
|
|
|
2447
2627
|
throw new Error("Mask dimensions must be positive integers");
|
|
2448
2628
|
}
|
|
2449
2629
|
const backing = this.makeCanvas(width, height);
|
|
2450
|
-
const image = new
|
|
2630
|
+
const image = new import_fabric9.FabricImage(backing, {
|
|
2451
2631
|
left: 0,
|
|
2452
2632
|
top: 0,
|
|
2453
2633
|
originX: "left",
|
|
@@ -2696,6 +2876,7 @@ var CanvasEditor = class {
|
|
|
2696
2876
|
fileAdapter;
|
|
2697
2877
|
imageProvider;
|
|
2698
2878
|
zoomLevel = 1;
|
|
2879
|
+
selectionStyle = { ...DEFAULT_SELECTION_STYLE };
|
|
2699
2880
|
mockup = null;
|
|
2700
2881
|
// The design's configured background. The live canvas background is forced
|
|
2701
2882
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
@@ -2712,7 +2893,7 @@ var CanvasEditor = class {
|
|
|
2712
2893
|
const widthPx = this.units.toPixels(config.width);
|
|
2713
2894
|
const heightPx = this.units.toPixels(config.height);
|
|
2714
2895
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2715
|
-
this.canvas = new
|
|
2896
|
+
this.canvas = new import_fabric10.Canvas(canvasElement, {
|
|
2716
2897
|
width: widthPx,
|
|
2717
2898
|
height: heightPx,
|
|
2718
2899
|
backgroundColor: this.designBackground,
|
|
@@ -2733,16 +2914,11 @@ var CanvasEditor = class {
|
|
|
2733
2914
|
this.layers.setHistoryCallback(() => this.history.save());
|
|
2734
2915
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
2735
2916
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
2736
|
-
this.patterns = new PatternManager(
|
|
2737
|
-
this.canvas,
|
|
2738
|
-
this.layers,
|
|
2739
|
-
this.history,
|
|
2740
|
-
this.events,
|
|
2741
|
-
config.patternSourceResolver
|
|
2742
|
-
);
|
|
2917
|
+
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2743
2918
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2744
2919
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
2745
2920
|
this.setupCanvasEvents();
|
|
2921
|
+
this.refreshSelectionStyle();
|
|
2746
2922
|
this.history.saveImmediate();
|
|
2747
2923
|
this.pages = new ProjectManager(this);
|
|
2748
2924
|
this.masks = new MaskController(this);
|
|
@@ -2750,7 +2926,7 @@ var CanvasEditor = class {
|
|
|
2750
2926
|
// ─── Layer Operations ────────────────────────────────
|
|
2751
2927
|
async addImage(url, options) {
|
|
2752
2928
|
try {
|
|
2753
|
-
const img = await
|
|
2929
|
+
const img = await import_fabric10.FabricImage.fromURL(
|
|
2754
2930
|
url,
|
|
2755
2931
|
{},
|
|
2756
2932
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -2775,7 +2951,7 @@ var CanvasEditor = class {
|
|
|
2775
2951
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2776
2952
|
const previous = layer.fabricObject;
|
|
2777
2953
|
try {
|
|
2778
|
-
const replacement = await
|
|
2954
|
+
const replacement = await import_fabric10.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2779
2955
|
replacement.set({
|
|
2780
2956
|
left: previous.left,
|
|
2781
2957
|
top: previous.top,
|
|
@@ -2804,7 +2980,7 @@ var CanvasEditor = class {
|
|
|
2804
2980
|
}
|
|
2805
2981
|
}
|
|
2806
2982
|
addText(text, options) {
|
|
2807
|
-
const textbox = new
|
|
2983
|
+
const textbox = new import_fabric10.Textbox(text, {
|
|
2808
2984
|
fontSize: 32,
|
|
2809
2985
|
fontFamily: "Arial",
|
|
2810
2986
|
fill: "#000000",
|
|
@@ -2843,10 +3019,10 @@ var CanvasEditor = class {
|
|
|
2843
3019
|
return value === void 0 ? token : escapeXml(value);
|
|
2844
3020
|
})
|
|
2845
3021
|
);
|
|
2846
|
-
const { objects, options } = await (0,
|
|
3022
|
+
const { objects, options } = await (0, import_fabric10.loadSVGFromString)(resolved);
|
|
2847
3023
|
const validObjects = objects.filter((object) => object !== null);
|
|
2848
3024
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2849
|
-
const group =
|
|
3025
|
+
const group = import_fabric10.util.groupSVGElements(validObjects, options);
|
|
2850
3026
|
group.set({
|
|
2851
3027
|
left: this.canvas.getWidth() / 2,
|
|
2852
3028
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2891,9 +3067,7 @@ var CanvasEditor = class {
|
|
|
2891
3067
|
const layer = this.layers.get(id);
|
|
2892
3068
|
if (!layer) return null;
|
|
2893
3069
|
const clone = await layer.fabricObject.clone();
|
|
2894
|
-
|
|
2895
|
-
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2896
|
-
}
|
|
3070
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2897
3071
|
clone.setCoords();
|
|
2898
3072
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
2899
3073
|
copy.meta = structuredClone(layer.meta);
|
|
@@ -2906,6 +3080,7 @@ var CanvasEditor = class {
|
|
|
2906
3080
|
evented: !layer.locked,
|
|
2907
3081
|
opacity: layer.opacity
|
|
2908
3082
|
});
|
|
3083
|
+
this.patterns.attachTo(copy);
|
|
2909
3084
|
this.canvas.setActiveObject(clone);
|
|
2910
3085
|
this.canvas.requestRenderAll();
|
|
2911
3086
|
this.history.save();
|
|
@@ -2919,10 +3094,10 @@ var CanvasEditor = class {
|
|
|
2919
3094
|
const next = { ...previous, ...adjustments };
|
|
2920
3095
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
2921
3096
|
image.filters = [
|
|
2922
|
-
new
|
|
2923
|
-
new
|
|
2924
|
-
new
|
|
2925
|
-
new
|
|
3097
|
+
new import_fabric10.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
3098
|
+
new import_fabric10.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
3099
|
+
new import_fabric10.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
3100
|
+
new import_fabric10.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
2926
3101
|
];
|
|
2927
3102
|
layer.meta.imageAdjustments = next;
|
|
2928
3103
|
image.applyFilters();
|
|
@@ -2939,7 +3114,7 @@ var CanvasEditor = class {
|
|
|
2939
3114
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2940
3115
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2941
3116
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2942
|
-
const group = new
|
|
3117
|
+
const group = new import_fabric10.Group(objects);
|
|
2943
3118
|
const grouped = this.layers.add("group", group, name);
|
|
2944
3119
|
grouped.meta.groupChildren = childData;
|
|
2945
3120
|
this.layers.select(grouped.id);
|
|
@@ -2958,7 +3133,7 @@ var CanvasEditor = class {
|
|
|
2958
3133
|
const objects = group.removeAll();
|
|
2959
3134
|
this.layers.remove(id);
|
|
2960
3135
|
const restored = objects.map((object, index) => {
|
|
2961
|
-
|
|
3136
|
+
import_fabric10.util.addTransformToObject(object, transform);
|
|
2962
3137
|
object.setCoords();
|
|
2963
3138
|
const data = childData[index];
|
|
2964
3139
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -2985,7 +3160,7 @@ var CanvasEditor = class {
|
|
|
2985
3160
|
}
|
|
2986
3161
|
try {
|
|
2987
3162
|
await deserializeEditor(this, state);
|
|
2988
|
-
this.patterns.
|
|
3163
|
+
await this.patterns.rehydrateAll();
|
|
2989
3164
|
} catch (error) {
|
|
2990
3165
|
if (!managedByHistory) {
|
|
2991
3166
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3014,7 +3189,7 @@ var CanvasEditor = class {
|
|
|
3014
3189
|
const layer = this.layers.get(id);
|
|
3015
3190
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
3016
3191
|
try {
|
|
3017
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
3192
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric10.FabricImage) {
|
|
3018
3193
|
const image = await layer.fabricObject.clone();
|
|
3019
3194
|
image.set({
|
|
3020
3195
|
left: 0,
|
|
@@ -3034,7 +3209,11 @@ var CanvasEditor = class {
|
|
|
3034
3209
|
cloneObjects: false
|
|
3035
3210
|
});
|
|
3036
3211
|
}
|
|
3037
|
-
return await exportIsolatedPNG(
|
|
3212
|
+
return await exportIsolatedPNG(
|
|
3213
|
+
this.canvas,
|
|
3214
|
+
[layer.renderProxy ?? layer.fabricObject],
|
|
3215
|
+
options
|
|
3216
|
+
);
|
|
3038
3217
|
} catch (error) {
|
|
3039
3218
|
this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
|
|
3040
3219
|
throw error;
|
|
@@ -3286,7 +3465,7 @@ var CanvasEditor = class {
|
|
|
3286
3465
|
return;
|
|
3287
3466
|
}
|
|
3288
3467
|
try {
|
|
3289
|
-
const image = await
|
|
3468
|
+
const image = await import_fabric10.FabricImage.fromURL(
|
|
3290
3469
|
url,
|
|
3291
3470
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3292
3471
|
{ originX: "left", originY: "top" }
|
|
@@ -3339,7 +3518,6 @@ var CanvasEditor = class {
|
|
|
3339
3518
|
const sx = widthPx / oldWidth;
|
|
3340
3519
|
const sy = heightPx / oldHeight;
|
|
3341
3520
|
for (const layer of this.layers.getAll()) {
|
|
3342
|
-
if (layer.meta.pattern) continue;
|
|
3343
3521
|
const object = layer.fabricObject;
|
|
3344
3522
|
object.set({
|
|
3345
3523
|
left: (object.left ?? 0) * sx,
|
|
@@ -3360,7 +3538,7 @@ var CanvasEditor = class {
|
|
|
3360
3538
|
}
|
|
3361
3539
|
}
|
|
3362
3540
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
3363
|
-
this.patterns.
|
|
3541
|
+
this.patterns.syncArea();
|
|
3364
3542
|
this.canvas.requestRenderAll();
|
|
3365
3543
|
this.history.save();
|
|
3366
3544
|
this.events.emit("canvas:modified", {});
|
|
@@ -3413,6 +3591,7 @@ var CanvasEditor = class {
|
|
|
3413
3591
|
const next = clamp(round2(level), MIN_ZOOM, MAX_ZOOM);
|
|
3414
3592
|
if (next === this.zoomLevel) return;
|
|
3415
3593
|
this.zoomLevel = next;
|
|
3594
|
+
this.refreshSelectionStyle();
|
|
3416
3595
|
this.events.emit("zoom:changed", { zoom: next });
|
|
3417
3596
|
}
|
|
3418
3597
|
/** @deprecated use setZoom — kept for backward compatibility. */
|
|
@@ -3557,12 +3736,35 @@ var CanvasEditor = class {
|
|
|
3557
3736
|
this.snapping.dispose();
|
|
3558
3737
|
this.crop.dispose();
|
|
3559
3738
|
this.history.dispose();
|
|
3560
|
-
|
|
3739
|
+
this.patterns.dispose();
|
|
3561
3740
|
this.events.removeAllListeners();
|
|
3562
3741
|
this.canvas.dispose();
|
|
3563
3742
|
}
|
|
3743
|
+
// ─── Selection style ────────────────────────────────
|
|
3744
|
+
/** Current look of the selection frame and its handles. */
|
|
3745
|
+
getSelectionStyle() {
|
|
3746
|
+
return { ...this.selectionStyle };
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Restyles the selection frame and resize handles. Sizes are in screen
|
|
3750
|
+
* pixels and stay constant across zoom levels (see `SelectionStyle`).
|
|
3751
|
+
*/
|
|
3752
|
+
setSelectionStyle(style) {
|
|
3753
|
+
this.selectionStyle = { ...this.selectionStyle, ...style };
|
|
3754
|
+
this.refreshSelectionStyle();
|
|
3755
|
+
}
|
|
3756
|
+
refreshSelectionStyle() {
|
|
3757
|
+
applySelectionStyle(this.canvas, this.selectionStyle, this.zoomLevel);
|
|
3758
|
+
}
|
|
3564
3759
|
// ─── Private ────────────────────────────────────────
|
|
3565
3760
|
setupCanvasEvents() {
|
|
3761
|
+
this.canvas.on("object:added", (e) => {
|
|
3762
|
+
if (e.target) applyObjectSelectionStyle(e.target, this.selectionStyle, this.zoomLevel);
|
|
3763
|
+
});
|
|
3764
|
+
const styleActive = () => {
|
|
3765
|
+
const active = this.canvas.getActiveObject();
|
|
3766
|
+
if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
|
|
3767
|
+
};
|
|
3566
3768
|
this.canvas.on("object:modified", (e) => {
|
|
3567
3769
|
if (!e.target) return;
|
|
3568
3770
|
const layer = this.layers.findByObject(e.target);
|
|
@@ -3572,10 +3774,12 @@ var CanvasEditor = class {
|
|
|
3572
3774
|
}
|
|
3573
3775
|
});
|
|
3574
3776
|
this.canvas.on("selection:created", (e) => {
|
|
3777
|
+
styleActive();
|
|
3575
3778
|
const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
|
|
3576
3779
|
this.events.emit("selection:changed", { selected });
|
|
3577
3780
|
});
|
|
3578
3781
|
this.canvas.on("selection:updated", (e) => {
|
|
3782
|
+
styleActive();
|
|
3579
3783
|
const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
|
|
3580
3784
|
this.events.emit("selection:changed", { selected });
|
|
3581
3785
|
});
|
|
@@ -3657,6 +3861,7 @@ var AnnotationOverlay = class {
|
|
|
3657
3861
|
CropController,
|
|
3658
3862
|
DEFAULT_LAYER_SHADOW,
|
|
3659
3863
|
DEFAULT_PATTERN_CONFIG,
|
|
3864
|
+
DEFAULT_SELECTION_STYLE,
|
|
3660
3865
|
DEFAULT_TEXT_CURVE,
|
|
3661
3866
|
EventEmitter,
|
|
3662
3867
|
FontRegistry,
|
|
@@ -3675,15 +3880,14 @@ var AnnotationOverlay = class {
|
|
|
3675
3880
|
TEXTURE_MASK_IDS,
|
|
3676
3881
|
TEXTURE_MASK_SIZE,
|
|
3677
3882
|
TextCurveManager,
|
|
3883
|
+
TiledPatternObject,
|
|
3678
3884
|
UnitConverter,
|
|
3679
3885
|
applyAspectLock,
|
|
3680
3886
|
applyLayerShadow,
|
|
3681
|
-
|
|
3887
|
+
applyObjectSelectionStyle,
|
|
3888
|
+
applySelectionStyle,
|
|
3682
3889
|
buildCurvePathData,
|
|
3683
|
-
buildPatternDataURL,
|
|
3684
|
-
captureLocks,
|
|
3685
3890
|
clamp,
|
|
3686
|
-
clearPatternImageCache,
|
|
3687
3891
|
clearTextureMaskCache,
|
|
3688
3892
|
computeCoverPlacement,
|
|
3689
3893
|
computePrintAreaClip,
|
|
@@ -3702,7 +3906,6 @@ var AnnotationOverlay = class {
|
|
|
3702
3906
|
isMaskPresetId,
|
|
3703
3907
|
isShapeMaskId,
|
|
3704
3908
|
isTextureMaskId,
|
|
3705
|
-
loadPatternImage,
|
|
3706
3909
|
readLayerShadow,
|
|
3707
3910
|
renderTextureMask,
|
|
3708
3911
|
resetTransform,
|