@overtone-art/canvas-editor-core 0.3.2 → 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 +127 -46
- package/dist/index.d.ts +127 -46
- package/dist/index.global.js +53 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +464 -332
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +441 -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.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
|
|
14
|
+
import { Canvas, FabricImage as FabricImage3, Group, Textbox, filters, loadSVGFromString, util as util3 } from "fabric";
|
|
15
15
|
|
|
16
16
|
// src/events.ts
|
|
17
17
|
var EventEmitter = class {
|
|
@@ -63,6 +63,12 @@ var Layer = class {
|
|
|
63
63
|
/** Non-fabric data (e.g. pattern config) that must persist with the layer. */
|
|
64
64
|
meta;
|
|
65
65
|
fabricObject;
|
|
66
|
+
/**
|
|
67
|
+
* Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
|
|
68
|
+
* part of the canvas but never part of the document: it is not serialized, and
|
|
69
|
+
* the layer's real object stays the one every editor API talks to.
|
|
70
|
+
*/
|
|
71
|
+
renderProxy;
|
|
66
72
|
constructor(type, fabricObject, name, id) {
|
|
67
73
|
this.id = id ?? generateId();
|
|
68
74
|
this.type = type;
|
|
@@ -72,6 +78,7 @@ var Layer = class {
|
|
|
72
78
|
this.opacity = 1;
|
|
73
79
|
this.meta = {};
|
|
74
80
|
this.fabricObject = fabricObject;
|
|
81
|
+
this.renderProxy = null;
|
|
75
82
|
this.fabricObject._layerId = this.id;
|
|
76
83
|
}
|
|
77
84
|
hasMeta() {
|
|
@@ -120,6 +127,7 @@ var LayerManager = class {
|
|
|
120
127
|
if (index === -1) return false;
|
|
121
128
|
const layer = this.layers[index];
|
|
122
129
|
this.canvas.remove(layer.fabricObject);
|
|
130
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
123
131
|
this.layers.splice(index, 1);
|
|
124
132
|
this.events.emit("layer:removed", { layerId: id });
|
|
125
133
|
this.emitChanged();
|
|
@@ -143,6 +151,10 @@ var LayerManager = class {
|
|
|
143
151
|
evented: !layer.locked
|
|
144
152
|
});
|
|
145
153
|
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
154
|
+
if (layer.renderProxy) {
|
|
155
|
+
fabricObject.set({ opacity: 0 });
|
|
156
|
+
this.syncZOrder();
|
|
157
|
+
}
|
|
146
158
|
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
147
159
|
this.canvas.requestRenderAll();
|
|
148
160
|
this.events.emit("layer:modified", { layerId: id });
|
|
@@ -150,6 +162,36 @@ var LayerManager = class {
|
|
|
150
162
|
this.onPropertyChanged?.();
|
|
151
163
|
return true;
|
|
152
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Attach (or clear) the object drawn in place of a layer's own object. The
|
|
167
|
+
* proxy tracks the layer's stacking position, visibility and opacity, and is
|
|
168
|
+
* removed with the layer — it must never outlive or drift from its source.
|
|
169
|
+
*/
|
|
170
|
+
setRenderProxy(id, proxy) {
|
|
171
|
+
const layer = this.get(id);
|
|
172
|
+
if (!layer || layer.renderProxy === proxy) return;
|
|
173
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
174
|
+
layer.renderProxy = proxy;
|
|
175
|
+
if (proxy) {
|
|
176
|
+
proxy.set({ visible: layer.visible, opacity: layer.opacity });
|
|
177
|
+
this.canvas.add(proxy);
|
|
178
|
+
this.syncZOrder();
|
|
179
|
+
}
|
|
180
|
+
this.canvas.requestRenderAll();
|
|
181
|
+
this.emitChanged();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Re-stack every canvas object to match layer order, keeping each proxy
|
|
185
|
+
* directly above the source it stands in for. Canvas indices can't be derived
|
|
186
|
+
* from layer indices once proxies are in the array, so the order is rebuilt
|
|
187
|
+
* front-to-back instead of computed.
|
|
188
|
+
*/
|
|
189
|
+
syncZOrder() {
|
|
190
|
+
for (const layer of this.layers) {
|
|
191
|
+
this.canvas.bringObjectToFront(layer.fabricObject);
|
|
192
|
+
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
153
195
|
reorder(id, newIndex) {
|
|
154
196
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
155
197
|
if (oldIndex === -1) return false;
|
|
@@ -158,9 +200,7 @@ var LayerManager = class {
|
|
|
158
200
|
if (oldIndex === clamped) return false;
|
|
159
201
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
160
202
|
this.layers.splice(clamped, 0, layer);
|
|
161
|
-
this.
|
|
162
|
-
this.canvas.moveObjectTo(l.fabricObject, i);
|
|
163
|
-
});
|
|
203
|
+
this.syncZOrder();
|
|
164
204
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
165
205
|
this.emitChanged();
|
|
166
206
|
this.onPropertyChanged?.();
|
|
@@ -197,6 +237,7 @@ var LayerManager = class {
|
|
|
197
237
|
if (layer.visible === visible) return;
|
|
198
238
|
layer.visible = visible;
|
|
199
239
|
layer.fabricObject.visible = visible;
|
|
240
|
+
if (layer.renderProxy) layer.renderProxy.visible = visible;
|
|
200
241
|
this.canvas.requestRenderAll();
|
|
201
242
|
this.emitChanged();
|
|
202
243
|
this.onPropertyChanged?.();
|
|
@@ -218,7 +259,8 @@ var LayerManager = class {
|
|
|
218
259
|
const next = Math.max(0, Math.min(1, opacity));
|
|
219
260
|
if (layer.opacity === next) return;
|
|
220
261
|
layer.opacity = next;
|
|
221
|
-
layer.
|
|
262
|
+
if (layer.renderProxy) layer.renderProxy.opacity = next;
|
|
263
|
+
else layer.fabricObject.opacity = next;
|
|
222
264
|
this.canvas.requestRenderAll();
|
|
223
265
|
this.emitChanged();
|
|
224
266
|
this.onPropertyChanged?.();
|
|
@@ -266,6 +308,7 @@ var LayerManager = class {
|
|
|
266
308
|
clear() {
|
|
267
309
|
for (const layer of this.layers) {
|
|
268
310
|
this.canvas.remove(layer.fabricObject);
|
|
311
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
269
312
|
}
|
|
270
313
|
this.layers = [];
|
|
271
314
|
this.emitChanged();
|
|
@@ -799,199 +842,415 @@ var CropController = class {
|
|
|
799
842
|
};
|
|
800
843
|
var STROKE2 = "#22c55e";
|
|
801
844
|
|
|
802
|
-
// src/pattern.ts
|
|
845
|
+
// src/pattern/pattern-manager.ts
|
|
803
846
|
import { util } from "fabric";
|
|
847
|
+
|
|
848
|
+
// src/pattern/tiled-pattern-object.ts
|
|
849
|
+
import { FabricObject } from "fabric";
|
|
850
|
+
|
|
851
|
+
// src/pattern/tile-geometry.ts
|
|
804
852
|
var MAX_TILES_PER_AXIS = 200;
|
|
853
|
+
function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
854
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
855
|
+
const radius = cornerRadius(anchor, targetW, targetH);
|
|
856
|
+
const span = radius * 2;
|
|
857
|
+
const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
|
|
858
|
+
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
859
|
+
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
860
|
+
const cols = Math.ceil(span / tileW) + 2;
|
|
861
|
+
const rows = Math.ceil(span / tileH) + 2;
|
|
862
|
+
const halfCols = Math.ceil(cols / 2);
|
|
863
|
+
const halfRows = Math.ceil(rows / 2);
|
|
864
|
+
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
865
|
+
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
866
|
+
const placements = [];
|
|
867
|
+
for (let j = -halfRows; j <= halfRows; j++) {
|
|
868
|
+
for (let i = -halfCols; i <= halfCols; i++) {
|
|
869
|
+
let x = i * tileW + shiftX;
|
|
870
|
+
let y = j * tileH + shiftY;
|
|
871
|
+
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
872
|
+
x += tileW * (config.horizontalOffset / 100);
|
|
873
|
+
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
874
|
+
y += tileH * (config.horizontalOffset / 100);
|
|
875
|
+
}
|
|
876
|
+
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
877
|
+
placements.push({ x, y, rotation });
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return placements;
|
|
881
|
+
}
|
|
882
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
883
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
884
|
+
ctx.save();
|
|
885
|
+
ctx.translate(anchor.x, anchor.y);
|
|
886
|
+
ctx.rotate(config.angle * Math.PI / 180);
|
|
887
|
+
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
|
|
888
|
+
ctx.save();
|
|
889
|
+
ctx.translate(tile.x, tile.y);
|
|
890
|
+
ctx.rotate(tile.rotation);
|
|
891
|
+
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
892
|
+
ctx.restore();
|
|
893
|
+
}
|
|
894
|
+
ctx.restore();
|
|
895
|
+
}
|
|
896
|
+
function cornerRadius(origin, w, h) {
|
|
897
|
+
const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
|
|
898
|
+
const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
|
|
899
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
900
|
+
}
|
|
901
|
+
function clampOffset(value) {
|
|
902
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
903
|
+
return Math.max(-100, Math.min(100, value));
|
|
904
|
+
}
|
|
905
|
+
function mod2(n) {
|
|
906
|
+
return (n % 2 + 2) % 2;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/pattern/tiled-pattern-object.ts
|
|
910
|
+
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
911
|
+
var MAX_SNAPSHOT_SCALE = 8;
|
|
912
|
+
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
913
|
+
var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
914
|
+
static type = "TiledPattern";
|
|
915
|
+
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
916
|
+
source;
|
|
917
|
+
config;
|
|
918
|
+
snapshotEl = null;
|
|
919
|
+
snapshotScale = 0;
|
|
920
|
+
constructor(source, config, width, height) {
|
|
921
|
+
super({
|
|
922
|
+
// Explicit origin: fabric v7 defaults to `center`, and a centre-origin
|
|
923
|
+
// object placed at 0,0 covers a quarter of the canvas. That exact bug is
|
|
924
|
+
// why the old bake-into-the-layer pattern landed in the top-left corner.
|
|
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,
|
|
935
|
+
objectCaching: false,
|
|
936
|
+
hasControls: false,
|
|
937
|
+
hasBorders: false
|
|
938
|
+
});
|
|
939
|
+
this.source = source;
|
|
940
|
+
this.config = config;
|
|
941
|
+
}
|
|
942
|
+
/** Swap in a new config; the next render picks it up. */
|
|
943
|
+
setConfig(config) {
|
|
944
|
+
this.config = config;
|
|
945
|
+
this.dirty = true;
|
|
946
|
+
}
|
|
947
|
+
/** Resize to a new print area. */
|
|
948
|
+
setArea(width, height) {
|
|
949
|
+
this.set({ width, height });
|
|
950
|
+
this.setCoords();
|
|
951
|
+
}
|
|
952
|
+
/** Drop the cached source snapshot (e.g. the source was edited). */
|
|
953
|
+
invalidate() {
|
|
954
|
+
this.snapshotEl = null;
|
|
955
|
+
this.snapshotScale = 0;
|
|
956
|
+
this.dirty = true;
|
|
957
|
+
}
|
|
958
|
+
/** Free the offscreen snapshot. */
|
|
959
|
+
dispose() {
|
|
960
|
+
this.snapshotEl = null;
|
|
961
|
+
this.snapshotScale = 0;
|
|
962
|
+
}
|
|
963
|
+
_render(ctx) {
|
|
964
|
+
const width = this.width ?? 0;
|
|
965
|
+
const height = this.height ?? 0;
|
|
966
|
+
if (width <= 0 || height <= 0) return;
|
|
967
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
968
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * tileScale);
|
|
969
|
+
if (!snapshot) return;
|
|
970
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
971
|
+
ctx.save();
|
|
972
|
+
ctx.translate(-width / 2, -height / 2);
|
|
973
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
974
|
+
ctx.restore();
|
|
975
|
+
}
|
|
976
|
+
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
977
|
+
_toSVG() {
|
|
978
|
+
const width = this.width ?? 0;
|
|
979
|
+
const height = this.height ?? 0;
|
|
980
|
+
if (width <= 0 || height <= 0) return [];
|
|
981
|
+
const el = document.createElement("canvas");
|
|
982
|
+
el.width = Math.max(1, Math.round(width));
|
|
983
|
+
el.height = Math.max(1, Math.round(height));
|
|
984
|
+
const ctx = el.getContext("2d");
|
|
985
|
+
if (!ctx) return [];
|
|
986
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
987
|
+
const snapshot = this.ensureSnapshot(tileScale);
|
|
988
|
+
if (!snapshot) return [];
|
|
989
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
990
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
991
|
+
return [
|
|
992
|
+
`<image x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" `,
|
|
993
|
+
`xlink:href="${el.toDataURL("image/png")}"></image>
|
|
994
|
+
`
|
|
995
|
+
];
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
999
|
+
* registry, which cannot carry a live source reference. Export paths clone
|
|
1000
|
+
* every canvas object, so without this a print export would silently lose the
|
|
1001
|
+
* tiling. The copy shares the source (it only ever reads from it).
|
|
1002
|
+
*/
|
|
1003
|
+
clone() {
|
|
1004
|
+
const copy = new _TiledPatternObject(
|
|
1005
|
+
this.source,
|
|
1006
|
+
this.config,
|
|
1007
|
+
this.width ?? 0,
|
|
1008
|
+
this.height ?? 0
|
|
1009
|
+
);
|
|
1010
|
+
copy.set({
|
|
1011
|
+
left: this.left,
|
|
1012
|
+
top: this.top,
|
|
1013
|
+
visible: this.visible,
|
|
1014
|
+
opacity: this.opacity
|
|
1015
|
+
});
|
|
1016
|
+
return Promise.resolve(copy);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1020
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1021
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1022
|
+
*/
|
|
1023
|
+
toObject() {
|
|
1024
|
+
const plain = super.toObject();
|
|
1025
|
+
delete plain.source;
|
|
1026
|
+
return plain;
|
|
1027
|
+
}
|
|
1028
|
+
/** Tile size in canvas units, floored so a tiny tile can't spawn a huge loop. */
|
|
1029
|
+
tileSize(snapshot, tileScale, width, height) {
|
|
1030
|
+
const floor = Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1031
|
+
return {
|
|
1032
|
+
tileW: Math.max(floor, snapshot.width / this.snapshotScale * tileScale),
|
|
1033
|
+
tileH: Math.max(floor, snapshot.height / this.snapshotScale * tileScale)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/** Grid anchor: the source's centre, in this object's coordinates. */
|
|
1037
|
+
gridOrigin() {
|
|
1038
|
+
const centre = this.source.getCenterPoint();
|
|
1039
|
+
return { x: centre.x - (this.left ?? 0), y: centre.y - (this.top ?? 0) };
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one when it is
|
|
1043
|
+
* still sharp enough and the source has not changed.
|
|
1044
|
+
*/
|
|
1045
|
+
ensureSnapshot(scale) {
|
|
1046
|
+
const wanted = this.clampScale(scale);
|
|
1047
|
+
const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
|
|
1048
|
+
if (!stale) return this.snapshotEl;
|
|
1049
|
+
const source = this.source;
|
|
1050
|
+
const opacity = source.opacity;
|
|
1051
|
+
source.opacity = 1;
|
|
1052
|
+
try {
|
|
1053
|
+
const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
|
|
1054
|
+
if (!el.width || !el.height) return null;
|
|
1055
|
+
this.snapshotEl = el;
|
|
1056
|
+
this.snapshotScale = wanted;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return this.snapshotEl;
|
|
1059
|
+
} finally {
|
|
1060
|
+
source.opacity = opacity;
|
|
1061
|
+
source.dirty = false;
|
|
1062
|
+
}
|
|
1063
|
+
return this.snapshotEl;
|
|
1064
|
+
}
|
|
1065
|
+
/** Bound the snapshot by both a linear scale and a total pixel budget. */
|
|
1066
|
+
clampScale(scale) {
|
|
1067
|
+
const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
|
|
1068
|
+
const rect = this.source.getBoundingRect();
|
|
1069
|
+
const area = Math.max(1, rect.width * rect.height);
|
|
1070
|
+
const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
|
|
1071
|
+
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
function contextScale(ctx) {
|
|
1075
|
+
if (typeof ctx.getTransform !== "function") return 1;
|
|
1076
|
+
try {
|
|
1077
|
+
const t = ctx.getTransform();
|
|
1078
|
+
return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return 1;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/pattern/pattern-manager.ts
|
|
805
1085
|
var PatternManager = class {
|
|
806
|
-
constructor(canvas, layers, history, events
|
|
1086
|
+
constructor(canvas, layers, history, events) {
|
|
807
1087
|
this.canvas = canvas;
|
|
808
1088
|
this.layers = layers;
|
|
809
1089
|
this.history = history;
|
|
810
1090
|
this.events = events;
|
|
811
|
-
this.
|
|
1091
|
+
this.canvas.on("object:modified", this.onSourceModified);
|
|
1092
|
+
this.canvas.on("text:changed", this.onSourceModified);
|
|
1093
|
+
this.events.on("layer:removed", this.onLayerRemoved);
|
|
812
1094
|
}
|
|
813
1095
|
canvas;
|
|
814
1096
|
layers;
|
|
815
1097
|
history;
|
|
816
1098
|
events;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1099
|
+
proxies = /* @__PURE__ */ new Map();
|
|
1100
|
+
onSourceModified = (event) => {
|
|
1101
|
+
this.invalidateFor(event.target);
|
|
1102
|
+
};
|
|
1103
|
+
onLayerRemoved = ({ layerId }) => {
|
|
1104
|
+
const proxy = this.proxies.get(layerId);
|
|
1105
|
+
if (!proxy) return;
|
|
1106
|
+
proxy.dispose();
|
|
1107
|
+
this.proxies.delete(layerId);
|
|
1108
|
+
};
|
|
823
1109
|
isPattern(layerId) {
|
|
824
1110
|
return !!this.layers.get(layerId)?.meta.pattern;
|
|
825
1111
|
}
|
|
826
1112
|
getConfig(layerId) {
|
|
827
1113
|
return this.layers.get(layerId)?.meta.pattern?.config ?? null;
|
|
828
1114
|
}
|
|
829
|
-
/** Turn a
|
|
830
|
-
apply(layerId, config) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
config,
|
|
840
|
-
originalSrc: elementToDataURL(image) ?? image.getSrc(),
|
|
841
|
-
originalClip: clip ? clip.toObject() : null,
|
|
842
|
-
originalLocks: captureLocks(image),
|
|
843
|
-
original: {
|
|
844
|
-
left: image.left ?? 0,
|
|
845
|
-
top: image.top ?? 0,
|
|
846
|
-
scaleX: image.scaleX ?? 1,
|
|
847
|
-
scaleY: image.scaleY ?? 1,
|
|
848
|
-
width: image.width ?? 0,
|
|
849
|
-
height: image.height ?? 0,
|
|
850
|
-
angle: image.angle ?? 0,
|
|
851
|
-
cropX: image.cropX ?? 0,
|
|
852
|
-
cropY: image.cropY ?? 0
|
|
853
|
-
}
|
|
854
|
-
};
|
|
1115
|
+
/** Turn a layer into a repeating pattern, or update an existing one. */
|
|
1116
|
+
async apply(layerId, config) {
|
|
1117
|
+
const layer = this.layers.get(layerId);
|
|
1118
|
+
if (!layer) return;
|
|
1119
|
+
try {
|
|
1120
|
+
this.layers.setMeta(layerId, { pattern: { config } });
|
|
1121
|
+
const proxy = this.proxies.get(layerId);
|
|
1122
|
+
if (proxy) {
|
|
1123
|
+
proxy.setConfig(config);
|
|
1124
|
+
this.canvas.requestRenderAll();
|
|
855
1125
|
} else {
|
|
856
|
-
layer
|
|
857
|
-
}
|
|
858
|
-
try {
|
|
859
|
-
await this.renderLayer(layer);
|
|
860
|
-
} catch (err) {
|
|
861
|
-
if (firstEnable) delete layer.meta.pattern;
|
|
862
|
-
throw err;
|
|
1126
|
+
this.attach(layer, config);
|
|
863
1127
|
}
|
|
864
1128
|
this.history.save();
|
|
865
|
-
}
|
|
866
|
-
this.events.emit("error", { message: "Failed to apply
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
this.events.emit("error", { message: "Failed to apply pattern", error });
|
|
867
1131
|
throw error;
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Stretch every restored pattern layer back over the full print area and
|
|
872
|
-
* re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
|
|
873
|
-
* it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
|
|
874
|
-
* after a state restore, where the canvas may be a different display size
|
|
875
|
-
* than when the pattern was baked — and to repair states saved while a
|
|
876
|
-
* pattern could still be dragged out of the print area.
|
|
877
|
-
*/
|
|
878
|
-
repinAll() {
|
|
879
|
-
const cw = this.canvas.getWidth();
|
|
880
|
-
const ch = this.canvas.getHeight();
|
|
881
|
-
let changed = false;
|
|
882
|
-
for (const layer of this.layers.getAll()) {
|
|
883
|
-
if (!layer.meta.pattern || layer.type !== "image") continue;
|
|
884
|
-
const image = layer.fabricObject;
|
|
885
|
-
image.set({
|
|
886
|
-
left: 0,
|
|
887
|
-
top: 0,
|
|
888
|
-
angle: 0,
|
|
889
|
-
scaleX: cw / (image.width || cw),
|
|
890
|
-
scaleY: ch / (image.height || ch)
|
|
891
|
-
});
|
|
892
|
-
applyPatternLocks(image);
|
|
893
|
-
image.setCoords();
|
|
894
|
-
changed = true;
|
|
895
1132
|
}
|
|
896
|
-
if (changed) this.canvas.requestRenderAll();
|
|
897
1133
|
}
|
|
898
|
-
/**
|
|
899
|
-
disable(layerId) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
left: state.original.left,
|
|
908
|
-
top: state.original.top,
|
|
909
|
-
scaleX: state.original.scaleX,
|
|
910
|
-
scaleY: state.original.scaleY,
|
|
911
|
-
width: state.original.width,
|
|
912
|
-
height: state.original.height,
|
|
913
|
-
cropX: state.original.cropX,
|
|
914
|
-
cropY: state.original.cropY,
|
|
915
|
-
angle: state.original.angle
|
|
916
|
-
});
|
|
917
|
-
image.clipPath = state.originalClip ? (await util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
918
|
-
restoreLocks(image, state.originalLocks);
|
|
919
|
-
image.setCoords();
|
|
920
|
-
delete layer.meta.pattern;
|
|
1134
|
+
/** Drop the tiling and show the source again. */
|
|
1135
|
+
async disable(layerId) {
|
|
1136
|
+
const layer = this.layers.get(layerId);
|
|
1137
|
+
if (!layer?.meta.pattern) return;
|
|
1138
|
+
try {
|
|
1139
|
+
this.detach(layerId);
|
|
1140
|
+
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1141
|
+
layer.fabricObject.set({ opacity: layer.opacity });
|
|
1142
|
+
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
921
1143
|
this.canvas.requestRenderAll();
|
|
922
1144
|
this.history.save();
|
|
923
|
-
}
|
|
924
|
-
this.events.emit("error", { message: "Failed to clear
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
this.events.emit("error", { message: "Failed to clear pattern", error });
|
|
925
1147
|
throw error;
|
|
926
|
-
}
|
|
1148
|
+
}
|
|
927
1149
|
}
|
|
928
|
-
/**
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1150
|
+
/**
|
|
1151
|
+
* Rebuild every proxy after a state restore, migrating any layer that was
|
|
1152
|
+
* saved by the old bake-into-the-layer engine.
|
|
1153
|
+
*/
|
|
1154
|
+
async rehydrateAll() {
|
|
1155
|
+
this.clearProxies();
|
|
1156
|
+
for (const layer of this.layers.getAll()) {
|
|
1157
|
+
const state = layer.meta.pattern;
|
|
1158
|
+
if (!state) continue;
|
|
1159
|
+
try {
|
|
1160
|
+
if (isLegacyState(state)) {
|
|
1161
|
+
await unbakeLegacyLayer(layer, state);
|
|
1162
|
+
this.layers.setMeta(layer.id, { pattern: { config: state.config } });
|
|
1163
|
+
}
|
|
1164
|
+
this.attach(layer, state.config);
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
this.events.emit("error", { message: "Failed to restore pattern layer", error });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
this.canvas.requestRenderAll();
|
|
1170
|
+
}
|
|
1171
|
+
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1172
|
+
syncArea() {
|
|
1173
|
+
const width = this.canvas.getWidth();
|
|
1174
|
+
const height = this.canvas.getHeight();
|
|
1175
|
+
for (const proxy of this.proxies.values()) {
|
|
1176
|
+
proxy.setArea(width, height);
|
|
1177
|
+
proxy.invalidate();
|
|
1178
|
+
}
|
|
1179
|
+
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1180
|
+
}
|
|
1181
|
+
/** Drop a layer's cached source snapshot (its content changed). */
|
|
1182
|
+
invalidate(layerId) {
|
|
1183
|
+
const proxy = this.proxies.get(layerId);
|
|
1184
|
+
if (!proxy) return;
|
|
1185
|
+
proxy.invalidate();
|
|
1186
|
+
this.canvas.requestRenderAll();
|
|
937
1187
|
}
|
|
938
|
-
|
|
1188
|
+
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
1189
|
+
attachTo(layer) {
|
|
939
1190
|
const state = layer.meta.pattern;
|
|
940
|
-
if (!state) return;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1191
|
+
if (!state || this.proxies.has(layer.id)) return;
|
|
1192
|
+
this.attach(layer, state.config);
|
|
1193
|
+
}
|
|
1194
|
+
dispose() {
|
|
1195
|
+
this.canvas.off("object:modified", this.onSourceModified);
|
|
1196
|
+
this.canvas.off("text:changed", this.onSourceModified);
|
|
1197
|
+
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1198
|
+
this.clearProxies();
|
|
1199
|
+
}
|
|
1200
|
+
/** Release every proxy and the offscreen snapshot it holds. */
|
|
1201
|
+
clearProxies() {
|
|
1202
|
+
for (const proxy of this.proxies.values()) proxy.dispose();
|
|
1203
|
+
this.proxies.clear();
|
|
1204
|
+
}
|
|
1205
|
+
attach(layer, config) {
|
|
1206
|
+
const proxy = new TiledPatternObject(
|
|
1207
|
+
layer.fabricObject,
|
|
1208
|
+
config,
|
|
1209
|
+
this.canvas.getWidth(),
|
|
1210
|
+
this.canvas.getHeight()
|
|
957
1211
|
);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
top: 0,
|
|
962
|
-
scaleX: 1,
|
|
963
|
-
scaleY: 1,
|
|
964
|
-
width: cw,
|
|
965
|
-
height: ch,
|
|
966
|
-
cropX: 0,
|
|
967
|
-
cropY: 0,
|
|
968
|
-
angle: 0
|
|
969
|
-
});
|
|
970
|
-
image.clipPath = void 0;
|
|
971
|
-
applyPatternLocks(image);
|
|
972
|
-
image.setCoords();
|
|
1212
|
+
layer.fabricObject.set({ opacity: 0 });
|
|
1213
|
+
this.proxies.set(layer.id, proxy);
|
|
1214
|
+
this.layers.setRenderProxy(layer.id, proxy);
|
|
973
1215
|
this.canvas.requestRenderAll();
|
|
974
1216
|
}
|
|
1217
|
+
detach(layerId) {
|
|
1218
|
+
const proxy = this.proxies.get(layerId);
|
|
1219
|
+
if (!proxy) {
|
|
1220
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1224
|
+
proxy.dispose();
|
|
1225
|
+
this.proxies.delete(layerId);
|
|
1226
|
+
}
|
|
1227
|
+
invalidateFor(target) {
|
|
1228
|
+
if (!target) return;
|
|
1229
|
+
const layer = this.layers.findByObject(target);
|
|
1230
|
+
if (layer) this.invalidate(layer.id);
|
|
1231
|
+
}
|
|
975
1232
|
};
|
|
976
|
-
function
|
|
977
|
-
return
|
|
978
|
-
lockMovementX: obj.lockMovementX ?? false,
|
|
979
|
-
lockMovementY: obj.lockMovementY ?? false,
|
|
980
|
-
lockScalingX: obj.lockScalingX ?? false,
|
|
981
|
-
lockScalingY: obj.lockScalingY ?? false,
|
|
982
|
-
lockRotation: obj.lockRotation ?? false,
|
|
983
|
-
hasControls: obj.hasControls ?? true
|
|
984
|
-
};
|
|
1233
|
+
function isLegacyState(state) {
|
|
1234
|
+
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
985
1235
|
}
|
|
986
|
-
function
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1236
|
+
async function unbakeLegacyLayer(layer, state) {
|
|
1237
|
+
const image = layer.fabricObject;
|
|
1238
|
+
if (typeof image.setSrc !== "function" || !state.original) return;
|
|
1239
|
+
await image.setSrc(state.originalSrc);
|
|
1240
|
+
image.set({
|
|
1241
|
+
left: state.original.left,
|
|
1242
|
+
top: state.original.top,
|
|
1243
|
+
scaleX: state.original.scaleX,
|
|
1244
|
+
scaleY: state.original.scaleY,
|
|
1245
|
+
width: state.original.width,
|
|
1246
|
+
height: state.original.height,
|
|
1247
|
+
cropX: state.original.cropX,
|
|
1248
|
+
cropY: state.original.cropY,
|
|
1249
|
+
angle: state.original.angle
|
|
994
1250
|
});
|
|
1251
|
+
image.clipPath = state.originalClip ? (await util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1252
|
+
restoreLocks(image, state.originalLocks);
|
|
1253
|
+
image.setCoords();
|
|
995
1254
|
}
|
|
996
1255
|
function restoreLocks(obj, locks) {
|
|
997
1256
|
obj.set(
|
|
@@ -1005,121 +1264,6 @@ function restoreLocks(obj, locks) {
|
|
|
1005
1264
|
}
|
|
1006
1265
|
);
|
|
1007
1266
|
}
|
|
1008
|
-
function elementToDataURL(image) {
|
|
1009
|
-
try {
|
|
1010
|
-
const el = image.getElement();
|
|
1011
|
-
const w = el.naturalWidth || el.width;
|
|
1012
|
-
const h = el.naturalHeight || el.height;
|
|
1013
|
-
if (!w || !h) return null;
|
|
1014
|
-
const off = document.createElement("canvas");
|
|
1015
|
-
off.width = w;
|
|
1016
|
-
off.height = h;
|
|
1017
|
-
const ctx = off.getContext("2d");
|
|
1018
|
-
if (!ctx) return null;
|
|
1019
|
-
ctx.drawImage(el, 0, 0);
|
|
1020
|
-
return off.toDataURL("image/png");
|
|
1021
|
-
} catch {
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
var IMAGE_CACHE_MAX = 16;
|
|
1026
|
-
var imageCache = /* @__PURE__ */ new Map();
|
|
1027
|
-
function loadPatternImage(src, resolver) {
|
|
1028
|
-
const cached = imageCache.get(src);
|
|
1029
|
-
if (cached) {
|
|
1030
|
-
imageCache.delete(src);
|
|
1031
|
-
imageCache.set(src, cached);
|
|
1032
|
-
return cached;
|
|
1033
|
-
}
|
|
1034
|
-
const promise = decodeImage(src).catch(async (originalError) => {
|
|
1035
|
-
if (!resolver) throw originalError;
|
|
1036
|
-
const resolved = await resolver(src);
|
|
1037
|
-
if (!resolved || resolved === src) {
|
|
1038
|
-
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
1039
|
-
cause: originalError
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
return decodeImage(resolved);
|
|
1043
|
-
});
|
|
1044
|
-
promise.catch(() => {
|
|
1045
|
-
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
1046
|
-
});
|
|
1047
|
-
imageCache.set(src, promise);
|
|
1048
|
-
if (imageCache.size > IMAGE_CACHE_MAX) {
|
|
1049
|
-
const oldest = imageCache.keys().next().value;
|
|
1050
|
-
if (oldest !== void 0) imageCache.delete(oldest);
|
|
1051
|
-
}
|
|
1052
|
-
return promise;
|
|
1053
|
-
}
|
|
1054
|
-
function decodeImage(src) {
|
|
1055
|
-
return new Promise((resolve, reject) => {
|
|
1056
|
-
const img = new Image();
|
|
1057
|
-
img.crossOrigin = "anonymous";
|
|
1058
|
-
img.onload = () => resolve(img);
|
|
1059
|
-
img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
|
|
1060
|
-
img.src = src;
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
function clearPatternImageCache() {
|
|
1064
|
-
imageCache.clear();
|
|
1065
|
-
}
|
|
1066
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
1067
|
-
const img = await loadPatternImage(src, sourceResolver);
|
|
1068
|
-
const off = document.createElement("canvas");
|
|
1069
|
-
off.width = Math.max(1, Math.round(targetW));
|
|
1070
|
-
off.height = Math.max(1, Math.round(targetH));
|
|
1071
|
-
const ctx = off.getContext("2d");
|
|
1072
|
-
if (!ctx) return off.toDataURL("image/png");
|
|
1073
|
-
drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
|
|
1074
|
-
return off.toDataURL("image/png");
|
|
1075
|
-
}
|
|
1076
|
-
function computeTilePositions(config, targetW, targetH, baseW, baseH) {
|
|
1077
|
-
const diag = Math.sqrt(targetW * targetW + targetH * targetH);
|
|
1078
|
-
const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
|
|
1079
|
-
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
1080
|
-
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
1081
|
-
const cols = Math.ceil(diag / tileW) + 2;
|
|
1082
|
-
const rows = Math.ceil(diag / tileH) + 2;
|
|
1083
|
-
const halfCols = Math.ceil(cols / 2);
|
|
1084
|
-
const halfRows = Math.ceil(rows / 2);
|
|
1085
|
-
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
1086
|
-
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
1087
|
-
const placements = [];
|
|
1088
|
-
for (let j = -halfRows; j <= halfRows; j++) {
|
|
1089
|
-
for (let i = -halfCols; i <= halfCols; i++) {
|
|
1090
|
-
let x = i * tileW + shiftX;
|
|
1091
|
-
let y = j * tileH + shiftY;
|
|
1092
|
-
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
1093
|
-
x += tileW * (config.horizontalOffset / 100);
|
|
1094
|
-
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
1095
|
-
y += tileH * (config.horizontalOffset / 100);
|
|
1096
|
-
}
|
|
1097
|
-
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
1098
|
-
placements.push({ x, y, rotation });
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
return placements;
|
|
1102
|
-
}
|
|
1103
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
|
|
1104
|
-
ctx.save();
|
|
1105
|
-
ctx.translate(targetW / 2, targetH / 2);
|
|
1106
|
-
ctx.rotate(config.angle * Math.PI / 180);
|
|
1107
|
-
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
|
|
1108
|
-
ctx.save();
|
|
1109
|
-
ctx.translate(tile.x, tile.y);
|
|
1110
|
-
ctx.rotate(tile.rotation);
|
|
1111
|
-
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
1112
|
-
ctx.restore();
|
|
1113
|
-
}
|
|
1114
|
-
ctx.restore();
|
|
1115
|
-
}
|
|
1116
|
-
function clampOffset(value) {
|
|
1117
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1118
|
-
return Math.max(-100, Math.min(100, value));
|
|
1119
|
-
}
|
|
1120
|
-
function mod2(n) {
|
|
1121
|
-
return (n % 2 + 2) % 2;
|
|
1122
|
-
}
|
|
1123
1267
|
|
|
1124
1268
|
// src/text-curve.ts
|
|
1125
1269
|
import { Path } from "fabric";
|
|
@@ -1287,7 +1431,7 @@ var TextCurveManager = class {
|
|
|
1287
1431
|
};
|
|
1288
1432
|
|
|
1289
1433
|
// src/mask-presets/manager.ts
|
|
1290
|
-
import { FabricImage
|
|
1434
|
+
import { FabricImage, Path as Path2 } from "fabric";
|
|
1291
1435
|
|
|
1292
1436
|
// src/mask-presets/shapes.ts
|
|
1293
1437
|
var SHAPE_MASK_IDS = [
|
|
@@ -1560,7 +1704,7 @@ var MaskPresetManager = class {
|
|
|
1560
1704
|
scaleY: height / SHAPE_MASK_BOX
|
|
1561
1705
|
});
|
|
1562
1706
|
}
|
|
1563
|
-
return new
|
|
1707
|
+
return new FabricImage(renderTextureMask(id), {
|
|
1564
1708
|
...shared,
|
|
1565
1709
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1566
1710
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -2128,7 +2272,7 @@ var ProjectManager = class {
|
|
|
2128
2272
|
};
|
|
2129
2273
|
|
|
2130
2274
|
// src/mask.ts
|
|
2131
|
-
import { FabricImage as
|
|
2275
|
+
import { FabricImage as FabricImage2 } from "fabric";
|
|
2132
2276
|
var MaskRefinementError = class extends Error {
|
|
2133
2277
|
constructor(code, message, cause) {
|
|
2134
2278
|
super(message);
|
|
@@ -2160,7 +2304,7 @@ var MaskController = class {
|
|
|
2160
2304
|
throw new Error("Mask dimensions must be positive integers");
|
|
2161
2305
|
}
|
|
2162
2306
|
const backing = this.makeCanvas(width, height);
|
|
2163
|
-
const image = new
|
|
2307
|
+
const image = new FabricImage2(backing, {
|
|
2164
2308
|
left: 0,
|
|
2165
2309
|
top: 0,
|
|
2166
2310
|
originX: "left",
|
|
@@ -2447,13 +2591,7 @@ var CanvasEditor = class {
|
|
|
2447
2591
|
this.layers.setHistoryCallback(() => this.history.save());
|
|
2448
2592
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
2449
2593
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
2450
|
-
this.patterns = new PatternManager(
|
|
2451
|
-
this.canvas,
|
|
2452
|
-
this.layers,
|
|
2453
|
-
this.history,
|
|
2454
|
-
this.events,
|
|
2455
|
-
config.patternSourceResolver
|
|
2456
|
-
);
|
|
2594
|
+
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2457
2595
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2458
2596
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
2459
2597
|
this.setupCanvasEvents();
|
|
@@ -2465,7 +2603,7 @@ var CanvasEditor = class {
|
|
|
2465
2603
|
// ─── Layer Operations ────────────────────────────────
|
|
2466
2604
|
async addImage(url, options) {
|
|
2467
2605
|
try {
|
|
2468
|
-
const img = await
|
|
2606
|
+
const img = await FabricImage3.fromURL(
|
|
2469
2607
|
url,
|
|
2470
2608
|
{},
|
|
2471
2609
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -2490,7 +2628,7 @@ var CanvasEditor = class {
|
|
|
2490
2628
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2491
2629
|
const previous = layer.fabricObject;
|
|
2492
2630
|
try {
|
|
2493
|
-
const replacement = await
|
|
2631
|
+
const replacement = await FabricImage3.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2494
2632
|
replacement.set({
|
|
2495
2633
|
left: previous.left,
|
|
2496
2634
|
top: previous.top,
|
|
@@ -2606,9 +2744,7 @@ var CanvasEditor = class {
|
|
|
2606
2744
|
const layer = this.layers.get(id);
|
|
2607
2745
|
if (!layer) return null;
|
|
2608
2746
|
const clone = await layer.fabricObject.clone();
|
|
2609
|
-
|
|
2610
|
-
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2611
|
-
}
|
|
2747
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2612
2748
|
clone.setCoords();
|
|
2613
2749
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
2614
2750
|
copy.meta = structuredClone(layer.meta);
|
|
@@ -2621,6 +2757,7 @@ var CanvasEditor = class {
|
|
|
2621
2757
|
evented: !layer.locked,
|
|
2622
2758
|
opacity: layer.opacity
|
|
2623
2759
|
});
|
|
2760
|
+
this.patterns.attachTo(copy);
|
|
2624
2761
|
this.canvas.setActiveObject(clone);
|
|
2625
2762
|
this.canvas.requestRenderAll();
|
|
2626
2763
|
this.history.save();
|
|
@@ -2700,7 +2837,7 @@ var CanvasEditor = class {
|
|
|
2700
2837
|
}
|
|
2701
2838
|
try {
|
|
2702
2839
|
await deserializeEditor(this, state);
|
|
2703
|
-
this.patterns.
|
|
2840
|
+
await this.patterns.rehydrateAll();
|
|
2704
2841
|
} catch (error) {
|
|
2705
2842
|
if (!managedByHistory) {
|
|
2706
2843
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -2729,7 +2866,7 @@ var CanvasEditor = class {
|
|
|
2729
2866
|
const layer = this.layers.get(id);
|
|
2730
2867
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
2731
2868
|
try {
|
|
2732
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
2869
|
+
if (options.resolution === "source" && layer.fabricObject instanceof FabricImage3) {
|
|
2733
2870
|
const image = await layer.fabricObject.clone();
|
|
2734
2871
|
image.set({
|
|
2735
2872
|
left: 0,
|
|
@@ -2749,7 +2886,11 @@ var CanvasEditor = class {
|
|
|
2749
2886
|
cloneObjects: false
|
|
2750
2887
|
});
|
|
2751
2888
|
}
|
|
2752
|
-
return await exportIsolatedPNG(
|
|
2889
|
+
return await exportIsolatedPNG(
|
|
2890
|
+
this.canvas,
|
|
2891
|
+
[layer.renderProxy ?? layer.fabricObject],
|
|
2892
|
+
options
|
|
2893
|
+
);
|
|
2753
2894
|
} catch (error) {
|
|
2754
2895
|
this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
|
|
2755
2896
|
throw error;
|
|
@@ -3001,7 +3142,7 @@ var CanvasEditor = class {
|
|
|
3001
3142
|
return;
|
|
3002
3143
|
}
|
|
3003
3144
|
try {
|
|
3004
|
-
const image = await
|
|
3145
|
+
const image = await FabricImage3.fromURL(
|
|
3005
3146
|
url,
|
|
3006
3147
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3007
3148
|
{ originX: "left", originY: "top" }
|
|
@@ -3054,7 +3195,6 @@ var CanvasEditor = class {
|
|
|
3054
3195
|
const sx = widthPx / oldWidth;
|
|
3055
3196
|
const sy = heightPx / oldHeight;
|
|
3056
3197
|
for (const layer of this.layers.getAll()) {
|
|
3057
|
-
if (layer.meta.pattern) continue;
|
|
3058
3198
|
const object = layer.fabricObject;
|
|
3059
3199
|
object.set({
|
|
3060
3200
|
left: (object.left ?? 0) * sx,
|
|
@@ -3075,7 +3215,7 @@ var CanvasEditor = class {
|
|
|
3075
3215
|
}
|
|
3076
3216
|
}
|
|
3077
3217
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
3078
|
-
this.patterns.
|
|
3218
|
+
this.patterns.syncArea();
|
|
3079
3219
|
this.canvas.requestRenderAll();
|
|
3080
3220
|
this.history.save();
|
|
3081
3221
|
this.events.emit("canvas:modified", {});
|
|
@@ -3273,7 +3413,7 @@ var CanvasEditor = class {
|
|
|
3273
3413
|
this.snapping.dispose();
|
|
3274
3414
|
this.crop.dispose();
|
|
3275
3415
|
this.history.dispose();
|
|
3276
|
-
|
|
3416
|
+
this.patterns.dispose();
|
|
3277
3417
|
this.events.removeAllListeners();
|
|
3278
3418
|
this.canvas.dispose();
|
|
3279
3419
|
}
|
|
@@ -3416,17 +3556,14 @@ export {
|
|
|
3416
3556
|
TEXTURE_MASK_IDS,
|
|
3417
3557
|
TEXTURE_MASK_SIZE,
|
|
3418
3558
|
TextCurveManager,
|
|
3559
|
+
TiledPatternObject,
|
|
3419
3560
|
UnitConverter,
|
|
3420
3561
|
applyAspectLock,
|
|
3421
3562
|
applyLayerShadow,
|
|
3422
3563
|
applyObjectSelectionStyle,
|
|
3423
|
-
applyPatternLocks,
|
|
3424
3564
|
applySelectionStyle,
|
|
3425
3565
|
buildCurvePathData,
|
|
3426
|
-
buildPatternDataURL,
|
|
3427
|
-
captureLocks,
|
|
3428
3566
|
clamp,
|
|
3429
|
-
clearPatternImageCache,
|
|
3430
3567
|
clearTextureMaskCache,
|
|
3431
3568
|
computeCoverPlacement,
|
|
3432
3569
|
computePrintAreaClip,
|
|
@@ -3445,7 +3582,6 @@ export {
|
|
|
3445
3582
|
isMaskPresetId,
|
|
3446
3583
|
isShapeMaskId,
|
|
3447
3584
|
isTextureMaskId,
|
|
3448
|
-
loadPatternImage,
|
|
3449
3585
|
readLayerShadow,
|
|
3450
3586
|
renderTextureMask,
|
|
3451
3587
|
resetTransform,
|