@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.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 FabricImage4, Group, Textbox, filters, loadSVGFromString, util as util3 } from "fabric";
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.layers.forEach((l, i) => {
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.fabricObject.opacity = next;
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, sourceResolver) {
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.sourceResolver = sourceResolver;
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
- sourceResolver;
818
- // Per-layer task chain. apply()/disable() both await an async setSrc on the
819
- // same fabric image; running two concurrently lets their setSrc resolutions
820
- // interleave (wrong image installed, original lost). Serialising per layer
821
- // guarantees the last-requested operation wins and state stays consistent.
822
- chains = /* @__PURE__ */ new Map();
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 plain image layer into a pattern, or update an existing one. */
830
- apply(layerId, config) {
831
- return this.enqueue(layerId, async () => {
832
- const layer = this.layers.get(layerId);
833
- if (!layer || layer.type !== "image") return;
834
- const image = layer.fabricObject;
835
- const firstEnable = !layer.meta.pattern;
836
- if (!layer.meta.pattern) {
837
- const clip = image.clipPath;
838
- layer.meta.pattern = {
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.meta.pattern.config = config;
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
- }).catch((error) => {
866
- this.events.emit("error", { message: "Failed to apply image pattern", error });
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
- /** Restore the original image and drop the pattern. */
899
- disable(layerId) {
900
- return this.enqueue(layerId, async () => {
901
- const layer = this.layers.get(layerId);
902
- const state = layer?.meta.pattern;
903
- if (!layer || !state) return;
904
- const image = layer.fabricObject;
905
- await image.setSrc(state.originalSrc);
906
- image.set({
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
- }).catch((error) => {
924
- this.events.emit("error", { message: "Failed to clear image pattern", error });
1145
+ } catch (error) {
1146
+ this.events.emit("error", { message: "Failed to clear pattern", error });
925
1147
  throw error;
926
- });
1148
+ }
927
1149
  }
928
- /** Run `task` after any in-flight work for this layer, regardless of outcome. */
929
- enqueue(layerId, task) {
930
- const prev = this.chains.get(layerId) ?? Promise.resolve();
931
- const next = prev.then(task, task);
932
- this.chains.set(
933
- layerId,
934
- next.catch(() => void 0)
935
- );
936
- return next;
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();
937
1170
  }
938
- async renderLayer(layer) {
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();
1187
+ }
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
- const image = layer.fabricObject;
942
- const cw = this.canvas.getWidth();
943
- const ch = this.canvas.getHeight();
944
- const scale = Math.max(1, state.config.scale ?? 100) / 100;
945
- const diag = Math.sqrt(cw * cw + ch * ch);
946
- const minTile = Math.max(2, diag / MAX_TILES_PER_AXIS);
947
- const tileW = Math.max(minTile, state.original.width * state.original.scaleX * scale);
948
- const tileH = Math.max(minTile, state.original.height * state.original.scaleY * scale);
949
- const dataUrl = await buildPatternDataURL(
950
- state.originalSrc,
951
- state.config,
952
- cw,
953
- ch,
954
- tileW,
955
- tileH,
956
- this.sourceResolver
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
- await image.setSrc(dataUrl);
959
- image.set({
960
- left: 0,
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 captureLocks(obj) {
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 applyPatternLocks(obj) {
987
- obj.set({
988
- lockMovementX: true,
989
- lockMovementY: true,
990
- lockScalingX: true,
991
- lockScalingY: true,
992
- lockRotation: true,
993
- hasControls: false
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 as FabricImage2, Path as Path2 } from "fabric";
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 FabricImage2(renderTextureMask(id), {
1707
+ return new FabricImage(renderTextureMask(id), {
1564
1708
  ...shared,
1565
1709
  scaleX: width / TEXTURE_MASK_SIZE,
1566
1710
  scaleY: height / TEXTURE_MASK_SIZE
@@ -1616,6 +1760,43 @@ function applyLayerShadow(object, config) {
1616
1760
  });
1617
1761
  }
1618
1762
 
1763
+ // src/selection-style.ts
1764
+ var DEFAULT_SELECTION_STYLE = {
1765
+ borderColor: "#c9a96e",
1766
+ cornerColor: "#c9a96e",
1767
+ cornerStrokeColor: "#101010",
1768
+ cornerSize: 11,
1769
+ cornerStyle: "circle",
1770
+ borderWidth: 1,
1771
+ borderDashArray: null,
1772
+ padding: 0,
1773
+ marqueeFill: "rgba(201, 169, 110, 0.12)"
1774
+ };
1775
+ function applyObjectSelectionStyle(object, style, zoom) {
1776
+ const scale = zoom > 0 ? 1 / zoom : 1;
1777
+ object.set({
1778
+ cornerSize: style.cornerSize * scale,
1779
+ cornerStyle: style.cornerStyle,
1780
+ cornerColor: style.cornerColor,
1781
+ cornerStrokeColor: style.cornerStrokeColor,
1782
+ // A filled handle is what makes the stroke colour visible at all.
1783
+ transparentCorners: false,
1784
+ borderColor: style.borderColor,
1785
+ borderScaleFactor: style.borderWidth * scale,
1786
+ borderDashArray: style.borderDashArray?.map((segment) => segment * scale) ?? null,
1787
+ padding: style.padding * scale
1788
+ });
1789
+ }
1790
+ function applySelectionStyle(canvas, style, zoom) {
1791
+ for (const object of canvas.getObjects()) applyObjectSelectionStyle(object, style, zoom);
1792
+ const active = canvas.getActiveObject();
1793
+ if (active) applyObjectSelectionStyle(active, style, zoom);
1794
+ canvas.selectionColor = style.marqueeFill;
1795
+ canvas.selectionBorderColor = style.borderColor;
1796
+ canvas.selectionLineWidth = style.borderWidth * (zoom > 0 ? 1 / zoom : 1);
1797
+ canvas.requestRenderAll();
1798
+ }
1799
+
1619
1800
  // src/transform.ts
1620
1801
  var SIDE_CONTROLS = ["ml", "mr", "mt", "mb"];
1621
1802
  function applyAspectLock(object, locked) {
@@ -2091,7 +2272,7 @@ var ProjectManager = class {
2091
2272
  };
2092
2273
 
2093
2274
  // src/mask.ts
2094
- import { FabricImage as FabricImage3 } from "fabric";
2275
+ import { FabricImage as FabricImage2 } from "fabric";
2095
2276
  var MaskRefinementError = class extends Error {
2096
2277
  constructor(code, message, cause) {
2097
2278
  super(message);
@@ -2123,7 +2304,7 @@ var MaskController = class {
2123
2304
  throw new Error("Mask dimensions must be positive integers");
2124
2305
  }
2125
2306
  const backing = this.makeCanvas(width, height);
2126
- const image = new FabricImage3(backing, {
2307
+ const image = new FabricImage2(backing, {
2127
2308
  left: 0,
2128
2309
  top: 0,
2129
2310
  originX: "left",
@@ -2372,6 +2553,7 @@ var CanvasEditor = class {
2372
2553
  fileAdapter;
2373
2554
  imageProvider;
2374
2555
  zoomLevel = 1;
2556
+ selectionStyle = { ...DEFAULT_SELECTION_STYLE };
2375
2557
  mockup = null;
2376
2558
  // The design's configured background. The live canvas background is forced
2377
2559
  // transparent while a mockup preview is shown, so this is the source of truth
@@ -2409,16 +2591,11 @@ var CanvasEditor = class {
2409
2591
  this.layers.setHistoryCallback(() => this.history.save());
2410
2592
  this.snapping = new SnapManager(this.canvas, this.events);
2411
2593
  this.crop = new CropController(this.canvas, this.history, this.events);
2412
- this.patterns = new PatternManager(
2413
- this.canvas,
2414
- this.layers,
2415
- this.history,
2416
- this.events,
2417
- config.patternSourceResolver
2418
- );
2594
+ this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
2419
2595
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
2420
2596
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
2421
2597
  this.setupCanvasEvents();
2598
+ this.refreshSelectionStyle();
2422
2599
  this.history.saveImmediate();
2423
2600
  this.pages = new ProjectManager(this);
2424
2601
  this.masks = new MaskController(this);
@@ -2426,7 +2603,7 @@ var CanvasEditor = class {
2426
2603
  // ─── Layer Operations ────────────────────────────────
2427
2604
  async addImage(url, options) {
2428
2605
  try {
2429
- const img = await FabricImage4.fromURL(
2606
+ const img = await FabricImage3.fromURL(
2430
2607
  url,
2431
2608
  {},
2432
2609
  { originX: "left", originY: "top", ...options }
@@ -2451,7 +2628,7 @@ var CanvasEditor = class {
2451
2628
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
2452
2629
  const previous = layer.fabricObject;
2453
2630
  try {
2454
- const replacement = await FabricImage4.fromURL(url, {}, { originX: "left", originY: "top" });
2631
+ const replacement = await FabricImage3.fromURL(url, {}, { originX: "left", originY: "top" });
2455
2632
  replacement.set({
2456
2633
  left: previous.left,
2457
2634
  top: previous.top,
@@ -2567,9 +2744,7 @@ var CanvasEditor = class {
2567
2744
  const layer = this.layers.get(id);
2568
2745
  if (!layer) return null;
2569
2746
  const clone = await layer.fabricObject.clone();
2570
- if (!layer.meta.pattern) {
2571
- clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
2572
- }
2747
+ clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
2573
2748
  clone.setCoords();
2574
2749
  const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
2575
2750
  copy.meta = structuredClone(layer.meta);
@@ -2582,6 +2757,7 @@ var CanvasEditor = class {
2582
2757
  evented: !layer.locked,
2583
2758
  opacity: layer.opacity
2584
2759
  });
2760
+ this.patterns.attachTo(copy);
2585
2761
  this.canvas.setActiveObject(clone);
2586
2762
  this.canvas.requestRenderAll();
2587
2763
  this.history.save();
@@ -2661,7 +2837,7 @@ var CanvasEditor = class {
2661
2837
  }
2662
2838
  try {
2663
2839
  await deserializeEditor(this, state);
2664
- this.patterns.repinAll();
2840
+ await this.patterns.rehydrateAll();
2665
2841
  } catch (error) {
2666
2842
  if (!managedByHistory) {
2667
2843
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -2690,7 +2866,7 @@ var CanvasEditor = class {
2690
2866
  const layer = this.layers.get(id);
2691
2867
  if (!layer) throw new Error(`Layer not found: ${id}`);
2692
2868
  try {
2693
- if (options.resolution === "source" && layer.fabricObject instanceof FabricImage4) {
2869
+ if (options.resolution === "source" && layer.fabricObject instanceof FabricImage3) {
2694
2870
  const image = await layer.fabricObject.clone();
2695
2871
  image.set({
2696
2872
  left: 0,
@@ -2710,7 +2886,11 @@ var CanvasEditor = class {
2710
2886
  cloneObjects: false
2711
2887
  });
2712
2888
  }
2713
- return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
2889
+ return await exportIsolatedPNG(
2890
+ this.canvas,
2891
+ [layer.renderProxy ?? layer.fabricObject],
2892
+ options
2893
+ );
2714
2894
  } catch (error) {
2715
2895
  this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
2716
2896
  throw error;
@@ -2962,7 +3142,7 @@ var CanvasEditor = class {
2962
3142
  return;
2963
3143
  }
2964
3144
  try {
2965
- const image = await FabricImage4.fromURL(
3145
+ const image = await FabricImage3.fromURL(
2966
3146
  url,
2967
3147
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
2968
3148
  { originX: "left", originY: "top" }
@@ -3015,7 +3195,6 @@ var CanvasEditor = class {
3015
3195
  const sx = widthPx / oldWidth;
3016
3196
  const sy = heightPx / oldHeight;
3017
3197
  for (const layer of this.layers.getAll()) {
3018
- if (layer.meta.pattern) continue;
3019
3198
  const object = layer.fabricObject;
3020
3199
  object.set({
3021
3200
  left: (object.left ?? 0) * sx,
@@ -3036,7 +3215,7 @@ var CanvasEditor = class {
3036
3215
  }
3037
3216
  }
3038
3217
  this.canvas.setDimensions({ width: widthPx, height: heightPx });
3039
- this.patterns.repinAll();
3218
+ this.patterns.syncArea();
3040
3219
  this.canvas.requestRenderAll();
3041
3220
  this.history.save();
3042
3221
  this.events.emit("canvas:modified", {});
@@ -3089,6 +3268,7 @@ var CanvasEditor = class {
3089
3268
  const next = clamp(round2(level), MIN_ZOOM, MAX_ZOOM);
3090
3269
  if (next === this.zoomLevel) return;
3091
3270
  this.zoomLevel = next;
3271
+ this.refreshSelectionStyle();
3092
3272
  this.events.emit("zoom:changed", { zoom: next });
3093
3273
  }
3094
3274
  /** @deprecated use setZoom — kept for backward compatibility. */
@@ -3233,12 +3413,35 @@ var CanvasEditor = class {
3233
3413
  this.snapping.dispose();
3234
3414
  this.crop.dispose();
3235
3415
  this.history.dispose();
3236
- clearPatternImageCache();
3416
+ this.patterns.dispose();
3237
3417
  this.events.removeAllListeners();
3238
3418
  this.canvas.dispose();
3239
3419
  }
3420
+ // ─── Selection style ────────────────────────────────
3421
+ /** Current look of the selection frame and its handles. */
3422
+ getSelectionStyle() {
3423
+ return { ...this.selectionStyle };
3424
+ }
3425
+ /**
3426
+ * Restyles the selection frame and resize handles. Sizes are in screen
3427
+ * pixels and stay constant across zoom levels (see `SelectionStyle`).
3428
+ */
3429
+ setSelectionStyle(style) {
3430
+ this.selectionStyle = { ...this.selectionStyle, ...style };
3431
+ this.refreshSelectionStyle();
3432
+ }
3433
+ refreshSelectionStyle() {
3434
+ applySelectionStyle(this.canvas, this.selectionStyle, this.zoomLevel);
3435
+ }
3240
3436
  // ─── Private ────────────────────────────────────────
3241
3437
  setupCanvasEvents() {
3438
+ this.canvas.on("object:added", (e) => {
3439
+ if (e.target) applyObjectSelectionStyle(e.target, this.selectionStyle, this.zoomLevel);
3440
+ });
3441
+ const styleActive = () => {
3442
+ const active = this.canvas.getActiveObject();
3443
+ if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
3444
+ };
3242
3445
  this.canvas.on("object:modified", (e) => {
3243
3446
  if (!e.target) return;
3244
3447
  const layer = this.layers.findByObject(e.target);
@@ -3248,10 +3451,12 @@ var CanvasEditor = class {
3248
3451
  }
3249
3452
  });
3250
3453
  this.canvas.on("selection:created", (e) => {
3454
+ styleActive();
3251
3455
  const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
3252
3456
  this.events.emit("selection:changed", { selected });
3253
3457
  });
3254
3458
  this.canvas.on("selection:updated", (e) => {
3459
+ styleActive();
3255
3460
  const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
3256
3461
  this.events.emit("selection:changed", { selected });
3257
3462
  });
@@ -3332,6 +3537,7 @@ export {
3332
3537
  CropController,
3333
3538
  DEFAULT_LAYER_SHADOW,
3334
3539
  DEFAULT_PATTERN_CONFIG,
3540
+ DEFAULT_SELECTION_STYLE,
3335
3541
  DEFAULT_TEXT_CURVE,
3336
3542
  EventEmitter,
3337
3543
  FontRegistry,
@@ -3350,15 +3556,14 @@ export {
3350
3556
  TEXTURE_MASK_IDS,
3351
3557
  TEXTURE_MASK_SIZE,
3352
3558
  TextCurveManager,
3559
+ TiledPatternObject,
3353
3560
  UnitConverter,
3354
3561
  applyAspectLock,
3355
3562
  applyLayerShadow,
3356
- applyPatternLocks,
3563
+ applyObjectSelectionStyle,
3564
+ applySelectionStyle,
3357
3565
  buildCurvePathData,
3358
- buildPatternDataURL,
3359
- captureLocks,
3360
3566
  clamp,
3361
- clearPatternImageCache,
3362
3567
  clearTextureMaskCache,
3363
3568
  computeCoverPlacement,
3364
3569
  computePrintAreaClip,
@@ -3377,7 +3582,6 @@ export {
3377
3582
  isMaskPresetId,
3378
3583
  isShapeMaskId,
3379
3584
  isTextureMaskId,
3380
- loadPatternImage,
3381
3585
  readLayerShadow,
3382
3586
  renderTextureMask,
3383
3587
  resetTransform,