@pixldocs/canvas-renderer 0.5.6 → 0.5.8

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.js CHANGED
@@ -2932,12 +2932,117 @@ async function normalizeSvgImageDimensions(fabricImage, imageUrl, sourceFormat)
2932
2932
  fabricImage.setCoords();
2933
2933
  }
2934
2934
  }
2935
+ const EMPTY_IMAGE_PLACEHOLDER_URL = "/image-placeholder.png";
2936
+ let placeholderTileImage = null;
2937
+ let placeholderTilePromise = null;
2938
+ function loadPlaceholderTile() {
2939
+ if (placeholderTileImage) return Promise.resolve(placeholderTileImage);
2940
+ if (placeholderTilePromise) return placeholderTilePromise;
2941
+ placeholderTilePromise = new Promise((resolve, reject) => {
2942
+ const img = new Image();
2943
+ img.crossOrigin = "anonymous";
2944
+ img.onload = () => {
2945
+ placeholderTileImage = img;
2946
+ resolve(img);
2947
+ };
2948
+ img.onerror = (e) => {
2949
+ placeholderTilePromise = null;
2950
+ reject(e);
2951
+ };
2952
+ img.src = EMPTY_IMAGE_PLACEHOLDER_URL;
2953
+ });
2954
+ return placeholderTilePromise;
2955
+ }
2956
+ function isEmptyImagePlaceholderGroup(obj) {
2957
+ return obj instanceof fabric.Group && Boolean(obj.__emptyImagePlaceholder);
2958
+ }
2959
+ function stabilizePlaceholderGroup(group, width, height) {
2960
+ group.set({ width, height, scaleX: 1, scaleY: 1 });
2961
+ if (group.layoutManager) {
2962
+ if (typeof fabric.FixedLayout === "function") {
2963
+ group.layoutManager.strategy = new fabric.FixedLayout();
2964
+ } else {
2965
+ group.layoutManager.performLayout = () => {
2966
+ };
2967
+ }
2968
+ }
2969
+ group.setCoords();
2970
+ }
2971
+ function attachEmptyPlaceholderImage(group, width, height) {
2972
+ loadPlaceholderTile().then((htmlImg) => {
2973
+ var _a;
2974
+ const objects = group._objects;
2975
+ const bgRect = objects == null ? void 0 : objects.find((obj) => obj.__isPlaceholderFrame);
2976
+ if (!bgRect) return;
2977
+ const pattern = new fabric.Pattern({
2978
+ source: htmlImg,
2979
+ repeat: "repeat"
2980
+ });
2981
+ pattern.__isPlaceholderPattern = true;
2982
+ bgRect.set({ fill: pattern });
2983
+ bgRect.dirty = true;
2984
+ if (!group.clipPath) {
2985
+ const clip = new fabric.Rect({
2986
+ width,
2987
+ height,
2988
+ left: 0,
2989
+ top: 0,
2990
+ originX: "center",
2991
+ originY: "center",
2992
+ selectable: false,
2993
+ evented: false
2994
+ });
2995
+ clip.absolutePositioned = false;
2996
+ clip.excludeFromExport = true;
2997
+ group.clipPath = clip;
2998
+ }
2999
+ stabilizePlaceholderGroup(group, width, height);
3000
+ group.dirty = true;
3001
+ (_a = group.canvas) == null ? void 0 : _a.requestRenderAll();
3002
+ }).catch(() => {
3003
+ });
3004
+ }
3005
+ function updateEmptyPlaceholderLayout(group, width, height) {
3006
+ const objects = group._objects;
3007
+ const backgroundRect = objects == null ? void 0 : objects.find((obj) => obj.__isPlaceholderFrame);
3008
+ const cropData = group.__cropData;
3009
+ if (cropData) {
3010
+ cropData.frameW = width;
3011
+ cropData.frameH = height;
3012
+ }
3013
+ backgroundRect == null ? void 0 : backgroundRect.set({
3014
+ width,
3015
+ height,
3016
+ left: 0,
3017
+ top: 0,
3018
+ originX: "center",
3019
+ originY: "center"
3020
+ });
3021
+ if (backgroundRect) backgroundRect.dirty = true;
3022
+ if (backgroundRect && !(backgroundRect.fill instanceof fabric.Pattern)) {
3023
+ attachEmptyPlaceholderImage(group, width, height);
3024
+ }
3025
+ if (group.clipPath && (group.clipPath instanceof fabric.Rect || group.clipPath instanceof fabric.Ellipse)) {
3026
+ group.clipPath.set({
3027
+ left: 0,
3028
+ top: 0,
3029
+ originX: "center",
3030
+ originY: "center",
3031
+ ...group.clipPath instanceof fabric.Ellipse ? { rx: width / 2, ry: height / 2 } : { width, height }
3032
+ });
3033
+ group.clipPath.setCoords();
3034
+ group.clipPath.dirty = true;
3035
+ }
3036
+ stabilizePlaceholderGroup(group, width, height);
3037
+ group.dirty = true;
3038
+ }
2935
3039
  function createImagePlaceholder(element) {
2936
3040
  const visualWidth = Number(element.width) * (element.scaleX ?? 1);
2937
3041
  const visualHeight = Number(element.height) * (element.scaleY ?? 1);
3042
+ const hasImage = !!(element.src || element.imageUrl);
2938
3043
  const bgRect = new fabric.Rect({
2939
- originX: "left",
2940
- originY: "top",
3044
+ originX: "center",
3045
+ originY: "center",
2941
3046
  left: 0,
2942
3047
  top: 0,
2943
3048
  width: visualWidth,
@@ -2948,56 +3053,65 @@ function createImagePlaceholder(element) {
2948
3053
  rx: element.clipShape === "rounded" ? element.rx || 8 : 0,
2949
3054
  ry: element.clipShape === "rounded" ? element.ry || 8 : 0
2950
3055
  });
3056
+ bgRect.__isPlaceholderFrame = true;
2951
3057
  const group = new fabric.Group([bgRect], {
2952
- left: element.left,
2953
- top: element.top,
2954
- originX: "left",
2955
- originY: "top",
2956
- selectable: element.selectable,
2957
- evented: element.evented
2958
- });
2959
- return group;
2960
- }
2961
- function createImagePlaceholderForGroup(element) {
2962
- const frameWidth = Number(element.width) * (element.scaleX ?? 1);
2963
- const frameHeight = Number(element.height) * (element.scaleY ?? 1);
2964
- const bgRect = new fabric.Rect({
2965
- originX: "left",
2966
- originY: "top",
2967
- left: 0,
2968
- top: 0,
2969
- width: frameWidth,
2970
- height: frameHeight,
2971
- fill: "transparent",
2972
- stroke: "transparent",
2973
- strokeWidth: 0,
2974
- rx: element.clipShape === "rounded" ? element.rx || 8 : 0,
2975
- ry: element.clipShape === "rounded" ? element.ry || 8 : 0
2976
- });
2977
- const group = new fabric.Group([bgRect], {
2978
- left: element.left,
2979
- top: element.top,
3058
+ left: (element.left ?? 0) + visualWidth / 2,
3059
+ top: (element.top ?? 0) + visualHeight / 2,
2980
3060
  originX: "center",
2981
3061
  originY: "center",
2982
- width: frameWidth,
2983
- height: frameHeight,
3062
+ width: visualWidth,
3063
+ height: visualHeight,
2984
3064
  scaleX: 1,
2985
3065
  scaleY: 1,
2986
3066
  angle: element.angle ?? 0,
2987
3067
  opacity: element.opacity ?? 1,
2988
3068
  flipX: element.flipX ?? false,
2989
- flipY: element.flipY ?? false
3069
+ flipY: element.flipY ?? false,
3070
+ selectable: element.selectable,
3071
+ evented: element.evented,
3072
+ hasControls: true,
3073
+ hasBorders: true,
3074
+ interactive: true,
3075
+ subTargetCheck: false,
3076
+ objectCaching: false
2990
3077
  });
2991
- if (element.clipShape && element.clipShape !== "none") {
2992
- const clipPath = createImageClipPath(element, frameWidth, frameHeight);
2993
- if (clipPath) {
2994
- clipPath.absolutePositioned = false;
2995
- clipPath.excludeFromExport = true;
2996
- group.clipPath = clipPath;
2997
- }
3078
+ group.__emptyImagePlaceholder = true;
3079
+ group.__imageSrc = "";
3080
+ group.__cropData = {
3081
+ shape: element.clipShape === "circle" ? "circle" : element.clipShape === "rounded" ? "roundRect" : "rect",
3082
+ rx: element.clipShape === "rounded" ? element.rx || 0.1 : 0,
3083
+ frameW: visualWidth,
3084
+ frameH: visualHeight,
3085
+ fit: "cover",
3086
+ _img: null,
3087
+ _border: null,
3088
+ _placeholderFrame: bgRect
3089
+ };
3090
+ group._ct = group._ct || {};
3091
+ group._ct.isCropGroup = true;
3092
+ group.__cropGroup = true;
3093
+ const clipPath = createImageClipPath(
3094
+ {
3095
+ ...element,
3096
+ clipShape: element.clipShape && element.clipShape !== "none" ? element.clipShape : "rectangle"
3097
+ },
3098
+ visualWidth,
3099
+ visualHeight
3100
+ );
3101
+ if (clipPath) {
3102
+ clipPath.absolutePositioned = false;
3103
+ clipPath.excludeFromExport = true;
3104
+ group.clipPath = clipPath;
3105
+ }
3106
+ stabilizePlaceholderGroup(group, visualWidth, visualHeight);
3107
+ if (!hasImage) {
3108
+ attachEmptyPlaceholderImage(group, visualWidth, visualHeight);
2998
3109
  }
2999
3110
  return group;
3000
3111
  }
3112
+ function createImagePlaceholderForGroup(element) {
3113
+ return createImagePlaceholder(element);
3114
+ }
3001
3115
  function createImageClipPath(element, imgWidth, imgHeight) {
3002
3116
  const clipShape = element.clipShape || "none";
3003
3117
  if (clipShape === "none") return void 0;
@@ -3035,7 +3149,328 @@ function createImageClipPath(element, imgWidth, imgHeight) {
3035
3149
  return void 0;
3036
3150
  }
3037
3151
  }
3038
- function clamp(v, min, max) {
3152
+ const isCropGroup = (obj) => {
3153
+ var _a;
3154
+ const g = obj;
3155
+ return Boolean(g && (g.__cropGroup || ((_a = g._ct) == null ? void 0 : _a.isCropGroup)));
3156
+ };
3157
+ function parseLuminance(fill) {
3158
+ if (typeof fill !== "string") return null;
3159
+ const f = fill.trim().toLowerCase();
3160
+ if (!f || f === "none" || f === "transparent") return null;
3161
+ if (f === "white" || f === "#fff" || f === "#ffffff") return 1;
3162
+ if (f === "black" || f === "#000" || f === "#000000") return 0;
3163
+ const hex = f.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
3164
+ if (hex) {
3165
+ const h = hex[1];
3166
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
3167
+ const r = parseInt(full.slice(0, 2), 16) / 255;
3168
+ const g = parseInt(full.slice(2, 4), 16) / 255;
3169
+ const b = parseInt(full.slice(4, 6), 16) / 255;
3170
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
3171
+ }
3172
+ const rgb = f.match(/^rgba?\(([^)]+)\)$/);
3173
+ if (rgb) {
3174
+ const parts = rgb[1].split(",").map((s) => parseFloat(s.trim()));
3175
+ if (parts.length >= 3) {
3176
+ const r = parts[0] / 255;
3177
+ const g = parts[1] / 255;
3178
+ const b = parts[2] / 255;
3179
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
3180
+ }
3181
+ }
3182
+ return null;
3183
+ }
3184
+ function getObjectRenderedSize(obj) {
3185
+ const bbox = typeof obj.getBoundingRect === "function" ? obj.getBoundingRect() : null;
3186
+ const width = Math.abs(Number(bbox == null ? void 0 : bbox.width) || (obj.width ?? 0) * Math.abs(obj.scaleX ?? 1));
3187
+ const height = Math.abs(Number(bbox == null ? void 0 : bbox.height) || (obj.height ?? 0) * Math.abs(obj.scaleY ?? 1));
3188
+ return { width, height };
3189
+ }
3190
+ function isLikelyFullFrameBackground(obj, luminance, viewBoxW, viewBoxH, totalObjects) {
3191
+ if (totalObjects <= 1 || viewBoxW <= 0 || viewBoxH <= 0 || luminance === null) return false;
3192
+ const { width, height } = getObjectRenderedSize(obj);
3193
+ const widthRatio = width / viewBoxW;
3194
+ const heightRatio = height / viewBoxH;
3195
+ const areaRatio = width * height / (viewBoxW * viewBoxH);
3196
+ return widthRatio >= 0.96 && heightRatio >= 0.96 && areaRatio >= 0.9;
3197
+ }
3198
+ async function loadSvgAsGroup(url) {
3199
+ const result = await fabric.loadSVGFromURL(url);
3200
+ const allObjects = (result.objects || []).filter(Boolean);
3201
+ const options = result.options || {};
3202
+ if (allObjects.length === 0) {
3203
+ throw new Error("SVG contained no parseable shapes");
3204
+ }
3205
+ const hintedViewBoxW = Number(options.width) || 0;
3206
+ const hintedViewBoxH = Number(options.height) || 0;
3207
+ const sourceLuminances = allObjects.map((o) => parseLuminance(o.fill));
3208
+ let objects = allObjects.filter((obj, i) => !isLikelyFullFrameBackground(
3209
+ obj,
3210
+ sourceLuminances[i],
3211
+ hintedViewBoxW,
3212
+ hintedViewBoxH,
3213
+ allObjects.length
3214
+ ));
3215
+ if (objects.length === 0) objects = allObjects;
3216
+ if (objects.length === 0) objects = allObjects;
3217
+ for (const obj of objects) {
3218
+ obj.fillRule = "evenodd";
3219
+ obj.fill = "#000";
3220
+ obj.stroke = null;
3221
+ obj.strokeWidth = 0;
3222
+ }
3223
+ const group = new fabric.Group(objects, {
3224
+ originX: "center",
3225
+ originY: "center",
3226
+ selectable: false,
3227
+ evented: false,
3228
+ hasControls: false,
3229
+ hasBorders: false
3230
+ });
3231
+ const viewBoxW = Number(options.width) || group.width || 1;
3232
+ const viewBoxH = Number(options.height) || group.height || 1;
3233
+ return { group, viewBoxW, viewBoxH };
3234
+ }
3235
+ function fitMaskGroupToFrame(maskGroup, frameW, frameH) {
3236
+ const viewBoxW = Number(maskGroup.__svgMaskViewBoxW) || maskGroup.width || 1;
3237
+ const viewBoxH = Number(maskGroup.__svgMaskViewBoxH) || maskGroup.height || 1;
3238
+ maskGroup.set({
3239
+ left: 0,
3240
+ top: 0,
3241
+ originX: "center",
3242
+ originY: "center",
3243
+ scaleX: frameW / viewBoxW,
3244
+ scaleY: frameH / viewBoxH,
3245
+ selectable: false,
3246
+ evented: false,
3247
+ hasControls: false,
3248
+ hasBorders: false
3249
+ });
3250
+ maskGroup.absolutePositioned = false;
3251
+ maskGroup.excludeFromExport = true;
3252
+ maskGroup.inverted = false;
3253
+ maskGroup.dirty = true;
3254
+ maskGroup.setCoords();
3255
+ }
3256
+ function isSvgMaskClipPath(clipPath) {
3257
+ return Boolean(clipPath && clipPath.__svgMask && clipPath instanceof fabric.Group);
3258
+ }
3259
+ function isLuminanceMaskClipPath(clipPath) {
3260
+ return Boolean(clipPath && clipPath.__svgMaskType === "luminance");
3261
+ }
3262
+ function syncSvgMaskClipPath(cropGroup) {
3263
+ var _a, _b;
3264
+ const clipPath = cropGroup.clipPath;
3265
+ if (!isCropGroup(cropGroup)) return;
3266
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3267
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3268
+ if (frameW <= 0 || frameH <= 0) return;
3269
+ if (isSvgMaskClipPath(clipPath)) {
3270
+ fitMaskGroupToFrame(clipPath, frameW, frameH);
3271
+ }
3272
+ }
3273
+ async function detectMaskType(svgUrl) {
3274
+ try {
3275
+ const res = await fetch(svgUrl);
3276
+ if (!res.ok) return "shape";
3277
+ const text = await res.text();
3278
+ const t = text.toLowerCase();
3279
+ if (t.includes("<lineargradient") || t.includes("<radialgradient")) return "luminance";
3280
+ if (t.includes("<image ") || t.includes("<image>")) return "luminance";
3281
+ if (t.includes('mask-type="luminance"') || t.includes("mask-type='luminance'")) return "luminance";
3282
+ if (t.includes("<filter")) return "luminance";
3283
+ if (/fill-opacity\s*=\s*["'](0?\.\d+)/.test(t)) return "luminance";
3284
+ if (/opacity\s*=\s*["'](0?\.\d+)/.test(t)) return "luminance";
3285
+ return "shape";
3286
+ } catch {
3287
+ return "shape";
3288
+ }
3289
+ }
3290
+ async function applySvgMaskToCropGroup(cropGroup, svgUrl) {
3291
+ var _a, _b, _c;
3292
+ if (!isCropGroup(cropGroup)) {
3293
+ throw new Error("Selected object is not a crop group / image");
3294
+ }
3295
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3296
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3297
+ if (frameW <= 0 || frameH <= 0) {
3298
+ throw new Error("Crop group has no frame dimensions");
3299
+ }
3300
+ const { group: maskGroup, viewBoxW, viewBoxH } = await loadSvgAsGroup(svgUrl);
3301
+ const mw = maskGroup.width ?? 0;
3302
+ const mh = maskGroup.height ?? 0;
3303
+ if (mw <= 0 || mh <= 0 || viewBoxW <= 0 || viewBoxH <= 0) {
3304
+ throw new Error("This mask has no usable shapes (try the 'Soft' mode instead)");
3305
+ }
3306
+ maskGroup.__svgMask = true;
3307
+ maskGroup.__svgMaskUrl = svgUrl;
3308
+ maskGroup.__svgMaskType = "shape";
3309
+ maskGroup.__svgMaskViewBoxW = viewBoxW;
3310
+ maskGroup.__svgMaskViewBoxH = viewBoxH;
3311
+ fitMaskGroupToFrame(maskGroup, frameW, frameH);
3312
+ cropGroup.clipPath = maskGroup;
3313
+ cropGroup.__svgMaskUrl = svgUrl;
3314
+ cropGroup.__svgMaskType = "shape";
3315
+ cropGroup.dirty = true;
3316
+ if (cropGroup._objects) {
3317
+ for (const child of cropGroup._objects) {
3318
+ child.dirty = true;
3319
+ }
3320
+ }
3321
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3322
+ }
3323
+ function loadSvgAsImage(svgUrl) {
3324
+ return new Promise((resolve, reject) => {
3325
+ const tryFetch = async () => {
3326
+ try {
3327
+ const res = await fetch(svgUrl, { mode: "cors" });
3328
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3329
+ const blob = await res.blob();
3330
+ const objectUrl = URL.createObjectURL(blob);
3331
+ const img2 = new Image();
3332
+ img2.onload = () => resolve(img2);
3333
+ img2.onerror = () => reject(new Error("Failed to load SVG (after fetch fallback)"));
3334
+ img2.src = objectUrl;
3335
+ } catch (e) {
3336
+ reject(new Error(`Failed to load SVG: ${(e == null ? void 0 : e.message) || "network error"}`));
3337
+ }
3338
+ };
3339
+ const img = new Image();
3340
+ img.crossOrigin = "anonymous";
3341
+ img.onload = () => resolve(img);
3342
+ img.onerror = () => tryFetch();
3343
+ img.src = svgUrl;
3344
+ });
3345
+ }
3346
+ async function buildLuminanceAlphaCanvas(svgUrl, frameW, frameH) {
3347
+ const w = Math.max(2, Math.round(frameW));
3348
+ const h = Math.max(2, Math.round(frameH));
3349
+ const img = await loadSvgAsImage(svgUrl);
3350
+ const canvas = document.createElement("canvas");
3351
+ canvas.width = w;
3352
+ canvas.height = h;
3353
+ const ctx = canvas.getContext("2d");
3354
+ if (!ctx) throw new Error("Failed to get 2D context for mask canvas");
3355
+ ctx.clearRect(0, 0, w, h);
3356
+ ctx.drawImage(img, 0, 0, w, h);
3357
+ let data;
3358
+ try {
3359
+ data = ctx.getImageData(0, 0, w, h);
3360
+ } catch (e) {
3361
+ throw new Error(
3362
+ "Could not read SVG pixels (CORS / tainted canvas). Try a different mask source."
3363
+ );
3364
+ }
3365
+ const px = data.data;
3366
+ for (let i = 0; i < px.length; i += 4) {
3367
+ const r = px[i];
3368
+ const g = px[i + 1];
3369
+ const b = px[i + 2];
3370
+ const a = px[i + 3];
3371
+ const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
3372
+ const alpha = lum / 255 * a;
3373
+ px[i] = 255;
3374
+ px[i + 1] = 255;
3375
+ px[i + 2] = 255;
3376
+ px[i + 3] = Math.round(alpha);
3377
+ }
3378
+ ctx.putImageData(data, 0, 0);
3379
+ return canvas;
3380
+ }
3381
+ async function applyLuminanceMaskToCropGroup(cropGroup, svgUrl) {
3382
+ var _a, _b, _c;
3383
+ if (!isCropGroup(cropGroup)) {
3384
+ throw new Error("Selected object is not a crop group / image");
3385
+ }
3386
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3387
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3388
+ if (frameW <= 0 || frameH <= 0) {
3389
+ throw new Error("Crop group has no frame dimensions");
3390
+ }
3391
+ const alphaCanvas = await buildLuminanceAlphaCanvas(svgUrl, frameW, frameH);
3392
+ const maskImg = new fabric.FabricImage(alphaCanvas, {
3393
+ originX: "center",
3394
+ originY: "center",
3395
+ left: 0,
3396
+ top: 0,
3397
+ selectable: false,
3398
+ evented: false,
3399
+ hasControls: false,
3400
+ hasBorders: false
3401
+ });
3402
+ maskImg.absolutePositioned = false;
3403
+ maskImg.excludeFromExport = true;
3404
+ maskImg.inverted = false;
3405
+ maskImg.__svgMask = true;
3406
+ maskImg.__svgMaskUrl = svgUrl;
3407
+ maskImg.__svgMaskType = "luminance";
3408
+ cropGroup.clipPath = maskImg;
3409
+ cropGroup.__svgMaskUrl = svgUrl;
3410
+ cropGroup.__svgMaskType = "luminance";
3411
+ cropGroup.dirty = true;
3412
+ if (cropGroup._objects) {
3413
+ for (const child of cropGroup._objects) {
3414
+ child.dirty = true;
3415
+ }
3416
+ }
3417
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3418
+ }
3419
+ async function applyMaskToCropGroup(cropGroup, svgUrl, maskType) {
3420
+ if (maskType === "luminance") {
3421
+ return applyLuminanceMaskToCropGroup(cropGroup, svgUrl);
3422
+ }
3423
+ return applySvgMaskToCropGroup(cropGroup, svgUrl);
3424
+ }
3425
+ function getAppliedSvgMaskUrl(obj) {
3426
+ if (!obj) return null;
3427
+ const url = obj.__svgMaskUrl;
3428
+ return typeof url === "string" ? url : null;
3429
+ }
3430
+ function getAppliedSvgMaskType(obj) {
3431
+ if (!obj) return null;
3432
+ const t = obj.__svgMaskType;
3433
+ return t === "luminance" || t === "shape" ? t : null;
3434
+ }
3435
+ function clearSvgMaskFromCropGroup(cropGroup) {
3436
+ var _a, _b, _c;
3437
+ if (!isCropGroup(cropGroup)) return;
3438
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3439
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3440
+ const rect = new fabric.Rect({
3441
+ width: frameW,
3442
+ height: frameH,
3443
+ left: 0,
3444
+ top: 0,
3445
+ originX: "center",
3446
+ originY: "center",
3447
+ selectable: false,
3448
+ evented: false,
3449
+ hasControls: false,
3450
+ hasBorders: false
3451
+ });
3452
+ rect.absolutePositioned = false;
3453
+ rect.excludeFromExport = true;
3454
+ cropGroup.clipPath = rect;
3455
+ delete cropGroup.__svgMaskUrl;
3456
+ delete cropGroup.__svgMaskType;
3457
+ cropGroup.dirty = true;
3458
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3459
+ }
3460
+ const svgMaskApply = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
3461
+ __proto__: null,
3462
+ applyLuminanceMaskToCropGroup,
3463
+ applyMaskToCropGroup,
3464
+ applySvgMaskToCropGroup,
3465
+ clearSvgMaskFromCropGroup,
3466
+ detectMaskType,
3467
+ getAppliedSvgMaskType,
3468
+ getAppliedSvgMaskUrl,
3469
+ isLuminanceMaskClipPath,
3470
+ isSvgMaskClipPath,
3471
+ syncSvgMaskClipPath
3472
+ }, Symbol.toStringTag, { value: "Module" }));
3473
+ function clamp$1(v, min, max) {
3039
3474
  return Math.max(min, Math.min(max, v));
3040
3475
  }
3041
3476
  function applyControlSizeForZoom(canvas, obj) {
@@ -3059,76 +3494,78 @@ function finalizeCropGroupCoords(g) {
3059
3494
  g.setCoords();
3060
3495
  }
3061
3496
  function updateCoverLayout(g) {
3497
+ var _a;
3062
3498
  const ct = g.__cropData;
3063
3499
  if (!ct) return;
3064
3500
  const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
3065
- if (!img) return;
3066
3501
  const minDim = Math.min(frameW, frameH);
3067
3502
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
3068
3503
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
3069
- const needsNewClipPath = !g.clipPath || shape === "circle" && !(g.clipPath instanceof fabric.Ellipse) || shape !== "circle" && !(g.clipPath instanceof fabric.Rect);
3070
- if (needsNewClipPath) {
3071
- const clip = shape === "circle" ? new fabric.Ellipse({
3072
- rx: frameW / 2,
3073
- ry: frameH / 2,
3074
- left: 0,
3075
- top: 0,
3076
- originX: "center",
3077
- originY: "center",
3078
- selectable: false,
3079
- evented: false,
3080
- hasControls: false,
3081
- hasBorders: false
3082
- }) : new fabric.Rect({
3083
- width: frameW,
3084
- height: frameH,
3085
- rx,
3086
- // rx is already calculated from ratio above
3087
- ry: rx,
3088
- // Ensure both are the same for uniform corners
3089
- left: 0,
3090
- top: 0,
3091
- originX: "center",
3092
- originY: "center",
3093
- selectable: false,
3094
- evented: false,
3095
- hasControls: false,
3096
- hasBorders: false
3097
- });
3098
- clip.absolutePositioned = false;
3099
- clip.excludeFromExport = true;
3100
- clip.setCoords();
3101
- clip.dirty = true;
3102
- g.clipPath = clip;
3103
- } else if (g.clipPath && typeof g.clipPath.set === "function") {
3104
- if (shape === "circle") {
3105
- g.clipPath.set({
3504
+ const currentClipPath = g.clipPath;
3505
+ const hasCustomSvgMask = Boolean(
3506
+ currentClipPath && (isSvgMaskClipPath(currentClipPath) || isLuminanceMaskClipPath(currentClipPath) || currentClipPath.__svgMask)
3507
+ );
3508
+ if (hasCustomSvgMask) {
3509
+ if (isSvgMaskClipPath(currentClipPath)) {
3510
+ syncSvgMaskClipPath(g);
3511
+ } else if (currentClipPath && typeof currentClipPath.set === "function") {
3512
+ const baseW = Math.max(1, Number(currentClipPath.width) || frameW);
3513
+ const baseH = Math.max(1, Number(currentClipPath.height) || frameH);
3514
+ currentClipPath.set({
3515
+ left: 0,
3516
+ top: 0,
3517
+ originX: "center",
3518
+ originY: "center",
3519
+ scaleX: frameW / baseW,
3520
+ scaleY: frameH / baseH,
3521
+ selectable: false,
3522
+ evented: false,
3523
+ hasControls: false,
3524
+ hasBorders: false
3525
+ });
3526
+ currentClipPath.absolutePositioned = false;
3527
+ currentClipPath.excludeFromExport = true;
3528
+ currentClipPath.dirty = true;
3529
+ currentClipPath.setCoords();
3530
+ }
3531
+ } else {
3532
+ const needsNewClipPath = !g.clipPath || shape === "circle" && !(g.clipPath instanceof fabric.Ellipse) || shape !== "circle" && !(g.clipPath instanceof fabric.Rect);
3533
+ if (needsNewClipPath) {
3534
+ const clip = shape === "circle" ? new fabric.Ellipse({
3106
3535
  rx: frameW / 2,
3107
3536
  ry: frameH / 2,
3108
3537
  left: 0,
3109
3538
  top: 0,
3110
3539
  originX: "center",
3111
3540
  originY: "center",
3112
- // CRITICAL: Keep clipPath non-interactive
3541
+ selectable: false,
3542
+ evented: false,
3543
+ hasControls: false,
3544
+ hasBorders: false
3545
+ }) : new fabric.Rect({
3546
+ width: frameW,
3547
+ height: frameH,
3548
+ rx,
3549
+ ry: rx,
3550
+ left: 0,
3551
+ top: 0,
3552
+ originX: "center",
3553
+ originY: "center",
3113
3554
  selectable: false,
3114
3555
  evented: false,
3115
3556
  hasControls: false,
3116
3557
  hasBorders: false
3117
3558
  });
3118
- } else {
3119
- const clipPathObj = g.clipPath;
3120
- const currentRx = clipPathObj.rx || 0;
3121
- const currentWidth = clipPathObj.width || 0;
3122
- const currentHeight = clipPathObj.height || 0;
3123
- const rxChanged = Math.abs(currentRx - rx) > 0.01;
3124
- const dimsChanged = Math.abs(currentWidth - frameW) > 0.1 || Math.abs(currentHeight - frameH) > 0.1;
3125
- if (rxChanged || dimsChanged) {
3126
- const newClip = new fabric.Rect({
3127
- width: frameW,
3128
- height: frameH,
3129
- rx,
3130
- ry: rx,
3131
- // Ensure both are identical for uniform corners
3559
+ clip.absolutePositioned = false;
3560
+ clip.excludeFromExport = true;
3561
+ clip.setCoords();
3562
+ clip.dirty = true;
3563
+ g.clipPath = clip;
3564
+ } else if (g.clipPath && typeof g.clipPath.set === "function") {
3565
+ if (shape === "circle") {
3566
+ g.clipPath.set({
3567
+ rx: frameW / 2,
3568
+ ry: frameH / 2,
3132
3569
  left: 0,
3133
3570
  top: 0,
3134
3571
  originX: "center",
@@ -3138,32 +3575,55 @@ function updateCoverLayout(g) {
3138
3575
  hasControls: false,
3139
3576
  hasBorders: false
3140
3577
  });
3141
- newClip.absolutePositioned = false;
3142
- newClip.excludeFromExport = true;
3143
- newClip.setCoords();
3144
- newClip.dirty = true;
3145
- g.clipPath = newClip;
3146
3578
  } else {
3147
- clipPathObj.set({
3148
- width: frameW,
3149
- height: frameH,
3150
- rx,
3151
- ry: rx,
3152
- left: 0,
3153
- top: 0,
3154
- originX: "center",
3155
- originY: "center",
3156
- selectable: false,
3157
- evented: false,
3158
- hasControls: false,
3159
- hasBorders: false
3160
- });
3161
- clipPathObj.setCoords();
3162
- clipPathObj.dirty = true;
3579
+ const clipPathObj = g.clipPath;
3580
+ const currentRx = clipPathObj.rx || 0;
3581
+ const currentWidth = clipPathObj.width || 0;
3582
+ const currentHeight = clipPathObj.height || 0;
3583
+ const rxChanged = Math.abs(currentRx - rx) > 0.01;
3584
+ const dimsChanged = Math.abs(currentWidth - frameW) > 0.1 || Math.abs(currentHeight - frameH) > 0.1;
3585
+ if (rxChanged || dimsChanged) {
3586
+ const newClip = new fabric.Rect({
3587
+ width: frameW,
3588
+ height: frameH,
3589
+ rx,
3590
+ ry: rx,
3591
+ left: 0,
3592
+ top: 0,
3593
+ originX: "center",
3594
+ originY: "center",
3595
+ selectable: false,
3596
+ evented: false,
3597
+ hasControls: false,
3598
+ hasBorders: false
3599
+ });
3600
+ newClip.absolutePositioned = false;
3601
+ newClip.excludeFromExport = true;
3602
+ newClip.setCoords();
3603
+ newClip.dirty = true;
3604
+ g.clipPath = newClip;
3605
+ } else {
3606
+ clipPathObj.set({
3607
+ width: frameW,
3608
+ height: frameH,
3609
+ rx,
3610
+ ry: rx,
3611
+ left: 0,
3612
+ top: 0,
3613
+ originX: "center",
3614
+ originY: "center",
3615
+ selectable: false,
3616
+ evented: false,
3617
+ hasControls: false,
3618
+ hasBorders: false
3619
+ });
3620
+ clipPathObj.setCoords();
3621
+ clipPathObj.dirty = true;
3622
+ }
3163
3623
  }
3624
+ g.clipPath.absolutePositioned = false;
3625
+ g.clipPath.excludeFromExport = true;
3164
3626
  }
3165
- g.clipPath.absolutePositioned = false;
3166
- g.clipPath.excludeFromExport = true;
3167
3627
  }
3168
3628
  if (border) {
3169
3629
  if (shape === "circle") {
@@ -3175,6 +3635,28 @@ function updateCoverLayout(g) {
3175
3635
  border.set({ width: frameW, height: frameH, rx: actualRx, ry: actualRx });
3176
3636
  }
3177
3637
  }
3638
+ if (!img) {
3639
+ const placeholderFrame = ct._placeholderFrame ?? ((_a = g._objects) == null ? void 0 : _a.find((obj) => obj.__isPlaceholderFrame));
3640
+ if (placeholderFrame && typeof placeholderFrame.set === "function") {
3641
+ placeholderFrame.set({
3642
+ width: frameW,
3643
+ height: frameH,
3644
+ left: 0,
3645
+ top: 0,
3646
+ originX: "center",
3647
+ originY: "center",
3648
+ ...placeholderFrame instanceof fabric.Rect ? { rx, ry: rx } : {}
3649
+ });
3650
+ placeholderFrame.dirty = true;
3651
+ ct._placeholderFrame = placeholderFrame;
3652
+ }
3653
+ finalizeCropGroupCoords(g);
3654
+ if (g.canvas) {
3655
+ g.setCoords();
3656
+ g.canvas.requestRenderAll();
3657
+ }
3658
+ return;
3659
+ }
3178
3660
  img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
3179
3661
  if (img.__panX !== void 0 || img.__panY !== void 0) {
3180
3662
  img._ct.panX = img.__panX ?? 0.5;
@@ -3196,8 +3678,8 @@ function updateCoverLayout(g) {
3196
3678
  const dispH = ih * finalScale;
3197
3679
  const overflowX = Math.max(0, dispW - frameW);
3198
3680
  const overflowY = Math.max(0, dispH - frameH);
3199
- const panX = clamp(img._ct.panX ?? 0.5, 0, 1);
3200
- const panY = clamp(img._ct.panY ?? 0.5, 0, 1);
3681
+ const panX = clamp$1(img._ct.panX ?? 0.5, 0, 1);
3682
+ const panY = clamp$1(img._ct.panY ?? 0.5, 0, 1);
3201
3683
  const offsetX = fitContain ? 0 : overflowX > 0 ? -overflowX * (panX - 0.5) : 0;
3202
3684
  const offsetY = fitContain ? 0 : overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
3203
3685
  img.set({ left: offsetX, top: offsetY });
@@ -3289,51 +3771,6 @@ function getRotatedControlCursor(controlKey, target) {
3289
3771
  const makeRotatedCursorStyleHandler = (controlKey) => {
3290
3772
  return (_eventData, _control, target) => getRotatedControlCursor(controlKey, target);
3291
3773
  };
3292
- function resizeFrameFromCorner(eventData, transform, _x, _y) {
3293
- var _a;
3294
- const g = transform.target;
3295
- const ct = g.__cropData;
3296
- if (!ct || !((_a = g._ct) == null ? void 0 : _a.isCropGroup)) return false;
3297
- const canvas = g.canvas;
3298
- if (!canvas) return false;
3299
- const e = getDomEvent(eventData);
3300
- if (!e) return false;
3301
- const pointer = canvas.getPointer(e);
3302
- g.setCoords();
3303
- const a = g.aCoords;
3304
- if (!a) return false;
3305
- const corner = transform.corner;
3306
- const anchor = getOppositeAnchor(a, corner);
3307
- const MIN_W = 20;
3308
- const MIN_H = 20;
3309
- const angle = g.angle || 0;
3310
- const deltaWorldX = pointer.x - anchor.x;
3311
- const deltaWorldY = pointer.y - anchor.y;
3312
- const localDelta = worldDeltaToLocal(deltaWorldX, deltaWorldY, angle);
3313
- const defaultSigns = getCornerDefaultSigns(corner);
3314
- const signX = Math.abs(localDelta.x) < 1e-3 ? defaultSigns.x : localDelta.x >= 0 ? 1 : -1;
3315
- const signY = Math.abs(localDelta.y) < 1e-3 ? defaultSigns.y : localDelta.y >= 0 ? 1 : -1;
3316
- const newW = Math.max(MIN_W, Math.abs(localDelta.x));
3317
- const newH = Math.max(MIN_H, Math.abs(localDelta.y));
3318
- ct.frameW = newW;
3319
- ct.frameH = newH;
3320
- const centerLocal = {
3321
- x: signX * (newW / 2),
3322
- y: signY * (newH / 2)
3323
- };
3324
- const centerWorld = localDeltaToWorld(centerLocal.x, centerLocal.y, angle);
3325
- g.set({
3326
- left: anchor.x + centerWorld.x,
3327
- top: anchor.y + centerWorld.y,
3328
- originX: "center",
3329
- originY: "center",
3330
- width: newW,
3331
- height: newH
3332
- });
3333
- updateCoverLayout(g);
3334
- canvas.requestRenderAll();
3335
- return true;
3336
- }
3337
3774
  function resizeFrameFromCornerUniform(eventData, transform, _x, _y) {
3338
3775
  var _a;
3339
3776
  const g = transform.target;
@@ -3413,7 +3850,7 @@ function resizeFrameFromSide(g, side, localDx, localDy) {
3413
3850
  }
3414
3851
  function installCanvaMaskControls(g) {
3415
3852
  const ct = g.__cropData;
3416
- if (ct) ct.fit = void 0;
3853
+ if (ct) ct.fit = ct.fit ?? "cover";
3417
3854
  g.setControlsVisibility({
3418
3855
  mt: true,
3419
3856
  mb: true,
@@ -3467,7 +3904,7 @@ function installCanvaMaskControls(g) {
3467
3904
  if (canvas && canvas.__editLockRef) {
3468
3905
  canvas.__editLockRef.current = true;
3469
3906
  }
3470
- return resizeFrameFromCorner(eventData, transform);
3907
+ return resizeFrameFromCornerUniform(eventData, transform);
3471
3908
  }
3472
3909
  });
3473
3910
  };
@@ -3477,47 +3914,6 @@ function installCanvaMaskControls(g) {
3477
3914
  g.controls.br = makeCornerControl("br", 0.5, 0.5, "nwse-resize");
3478
3915
  g.__hasCustomControls = true;
3479
3916
  }
3480
- function setSimpleScaleControls(g) {
3481
- if (!g.__cropGroup) return;
3482
- const ct = g.__cropData;
3483
- if (ct) {
3484
- updateCoverLayout(g);
3485
- }
3486
- g.lockScalingX = true;
3487
- g.lockScalingY = true;
3488
- g.setControlsVisibility({
3489
- mt: false,
3490
- mb: false,
3491
- ml: false,
3492
- mr: false,
3493
- tl: true,
3494
- tr: true,
3495
- bl: true,
3496
- br: true,
3497
- mtr: true
3498
- });
3499
- g.padding = 0;
3500
- const makeCornerControl = (key, x, y, cursor) => new fabric.Control({
3501
- x,
3502
- y,
3503
- cursorStyle: cursor,
3504
- cursorStyleHandler: makeRotatedCursorStyleHandler(key),
3505
- actionName: "resize",
3506
- actionHandler: (eventData, transform) => {
3507
- const t = transform.target;
3508
- if (t.canvas && t.canvas.__editLockRef) {
3509
- t.canvas.__editLockRef.current = true;
3510
- }
3511
- return resizeFrameFromCornerUniform(eventData, transform);
3512
- }
3513
- });
3514
- g.controls.tl = makeCornerControl("tl", -0.5, -0.5, "nwse-resize");
3515
- g.controls.tr = makeCornerControl("tr", 0.5, -0.5, "nesw-resize");
3516
- g.controls.bl = makeCornerControl("bl", -0.5, 0.5, "nesw-resize");
3517
- g.controls.br = makeCornerControl("br", 0.5, 0.5, "nwse-resize");
3518
- g.__hasCustomControls = true;
3519
- g.setCoords();
3520
- }
3521
3917
  async function createMaskedImageElement({
3522
3918
  url,
3523
3919
  image,
@@ -3700,9 +4096,405 @@ async function createMaskedImageElement({
3700
4096
  });
3701
4097
  g.setCoords();
3702
4098
  updateCoverLayout(g);
3703
- setSimpleScaleControls(g);
4099
+ installCanvaMaskControls(g);
3704
4100
  return g;
3705
4101
  }
4102
+ const CROP_MODE_FLAG = "__inCropMode";
4103
+ const CROP_GHOST_FLAG = "__cropGhost";
4104
+ const CROP_OUTLINE_FLAG = "__cropOutline";
4105
+ const CROP_HANDLERS_FLAG = "__cropHandlers";
4106
+ function isCropGroupInCropMode(g) {
4107
+ return Boolean(g[CROP_MODE_FLAG]);
4108
+ }
4109
+ function clamp(v, min, max) {
4110
+ return Math.max(min, Math.min(max, v));
4111
+ }
4112
+ function syncGhostTransform(g) {
4113
+ const ghost = g[CROP_GHOST_FLAG];
4114
+ const ct = g.__cropData;
4115
+ if (!ghost || !(ct == null ? void 0 : ct._img)) return;
4116
+ const img = ct._img;
4117
+ const finalScale = img.scaleX || 1;
4118
+ const gLeft = g.left ?? 0;
4119
+ const gTop = g.top ?? 0;
4120
+ const angle = g.angle ?? 0;
4121
+ const imgLocalLeft = img.left ?? 0;
4122
+ const imgLocalTop = img.top ?? 0;
4123
+ const rad = fabric.util.degreesToRadians(angle);
4124
+ const cos = Math.cos(rad);
4125
+ const sin = Math.sin(rad);
4126
+ const worldDx = imgLocalLeft * cos - imgLocalTop * sin;
4127
+ const worldDy = imgLocalLeft * sin + imgLocalTop * cos;
4128
+ ghost.set({
4129
+ left: gLeft + worldDx,
4130
+ top: gTop + worldDy,
4131
+ scaleX: finalScale,
4132
+ scaleY: finalScale,
4133
+ angle,
4134
+ originX: "center",
4135
+ originY: "center"
4136
+ });
4137
+ ghost.setCoords();
4138
+ }
4139
+ function syncOutlineTransform(g) {
4140
+ const outline = g[CROP_OUTLINE_FLAG];
4141
+ const ct = g.__cropData;
4142
+ if (!outline || !ct) return;
4143
+ outline.set({
4144
+ left: g.left ?? 0,
4145
+ top: g.top ?? 0,
4146
+ width: ct.frameW,
4147
+ height: ct.frameH,
4148
+ angle: g.angle ?? 0,
4149
+ originX: "center",
4150
+ originY: "center"
4151
+ });
4152
+ if (outline instanceof fabric.Rect) {
4153
+ const minDim = Math.min(ct.frameW, ct.frameH);
4154
+ const rxRatio = ct.rx ?? 0;
4155
+ let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
4156
+ rx = Math.max(0, Math.min(rx, ct.frameW / 2, ct.frameH / 2));
4157
+ outline.set({ rx, ry: rx });
4158
+ }
4159
+ outline.setCoords();
4160
+ }
4161
+ function installCropModeVisuals(g) {
4162
+ g.setControlsVisibility({
4163
+ mt: false,
4164
+ mb: false,
4165
+ ml: false,
4166
+ mr: false,
4167
+ tl: false,
4168
+ tr: false,
4169
+ bl: false,
4170
+ br: false,
4171
+ mtr: false
4172
+ });
4173
+ g.hasControls = false;
4174
+ g.lockScalingX = true;
4175
+ g.lockScalingY = true;
4176
+ g.lockRotation = true;
4177
+ g.lockMovementX = false;
4178
+ g.lockMovementY = false;
4179
+ g.borderColor = "hsl(256, 80%, 58%)";
4180
+ g.borderDashArray = [4, 4];
4181
+ g.setCoords();
4182
+ }
4183
+ function enterCropMode(g) {
4184
+ const ct = g.__cropData;
4185
+ if (!(ct == null ? void 0 : ct._img)) return false;
4186
+ if (g[CROP_MODE_FLAG]) return false;
4187
+ const canvas = g.canvas;
4188
+ if (!canvas) return false;
4189
+ g[CROP_MODE_FLAG] = true;
4190
+ const innerImg = ct._img;
4191
+ const imgEl = innerImg._element;
4192
+ if (!imgEl) {
4193
+ g[CROP_MODE_FLAG] = false;
4194
+ return false;
4195
+ }
4196
+ const ghost = new fabric.FabricImage(imgEl, {
4197
+ selectable: false,
4198
+ evented: false,
4199
+ hasControls: false,
4200
+ hasBorders: false,
4201
+ opacity: 0.35,
4202
+ originX: "center",
4203
+ originY: "center",
4204
+ objectCaching: false
4205
+ });
4206
+ ghost.excludeFromExport = true;
4207
+ ghost[CROP_GHOST_FLAG] = true;
4208
+ const outlineShape = ct.shape;
4209
+ const outline = outlineShape === "circle" ? new fabric.Ellipse({
4210
+ rx: ct.frameW / 2,
4211
+ ry: ct.frameH / 2,
4212
+ fill: "transparent",
4213
+ stroke: "hsl(256, 80%, 58%)",
4214
+ strokeWidth: 1.5,
4215
+ strokeDashArray: [6, 4],
4216
+ strokeUniform: true,
4217
+ selectable: false,
4218
+ evented: false,
4219
+ hasControls: false,
4220
+ hasBorders: false,
4221
+ originX: "center",
4222
+ originY: "center",
4223
+ objectCaching: false
4224
+ }) : new fabric.Rect({
4225
+ width: ct.frameW,
4226
+ height: ct.frameH,
4227
+ fill: "transparent",
4228
+ stroke: "hsl(256, 80%, 58%)",
4229
+ strokeWidth: 1.5,
4230
+ strokeDashArray: [6, 4],
4231
+ strokeUniform: true,
4232
+ selectable: false,
4233
+ evented: false,
4234
+ hasControls: false,
4235
+ hasBorders: false,
4236
+ originX: "center",
4237
+ originY: "center",
4238
+ objectCaching: false
4239
+ });
4240
+ outline.excludeFromExport = true;
4241
+ outline[CROP_OUTLINE_FLAG] = true;
4242
+ g[CROP_GHOST_FLAG] = ghost;
4243
+ g[CROP_OUTLINE_FLAG] = outline;
4244
+ const groupIndex = canvas._objects.indexOf(g);
4245
+ if (groupIndex >= 0) {
4246
+ canvas.insertAt(groupIndex, ghost);
4247
+ canvas.add(outline);
4248
+ canvas.bringObjectToFront(g);
4249
+ canvas.bringObjectToFront(outline);
4250
+ } else {
4251
+ canvas.add(ghost);
4252
+ canvas.add(g);
4253
+ canvas.add(outline);
4254
+ }
4255
+ syncGhostTransform(g);
4256
+ syncOutlineTransform(g);
4257
+ const groupAnchor = { left: g.left ?? 0, top: g.top ?? 0 };
4258
+ let panRafId = null;
4259
+ let pendingDx = 0;
4260
+ let pendingDy = 0;
4261
+ let lastDragDx = 0;
4262
+ let lastDragDy = 0;
4263
+ const flushPan = () => {
4264
+ panRafId = null;
4265
+ if (pendingDx === 0 && pendingDy === 0) return;
4266
+ const innerCt = g.__cropData;
4267
+ const img = innerCt == null ? void 0 : innerCt._img;
4268
+ if (!img) {
4269
+ pendingDx = 0;
4270
+ pendingDy = 0;
4271
+ return;
4272
+ }
4273
+ const angle = g.angle ?? 0;
4274
+ const rad = fabric.util.degreesToRadians(-angle);
4275
+ const cos = Math.cos(rad);
4276
+ const sin = Math.sin(rad);
4277
+ const localDx = pendingDx * cos - pendingDy * sin;
4278
+ const localDy = pendingDx * sin + pendingDy * cos;
4279
+ pendingDx = 0;
4280
+ pendingDy = 0;
4281
+ const iw = img.width || 1;
4282
+ const ih = img.height || 1;
4283
+ const finalScale = img.scaleX || 1;
4284
+ const dispW = iw * finalScale;
4285
+ const dispH = ih * finalScale;
4286
+ const overflowX = Math.max(0, dispW - innerCt.frameW);
4287
+ const overflowY = Math.max(0, dispH - innerCt.frameH);
4288
+ img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
4289
+ const ct2 = img._ct;
4290
+ if (overflowX > 0) ct2.panX = clamp((ct2.panX ?? 0.5) - localDx / overflowX, 0, 1);
4291
+ if (overflowY > 0) ct2.panY = clamp((ct2.panY ?? 0.5) - localDy / overflowY, 0, 1);
4292
+ updateCoverLayout(g);
4293
+ syncGhostTransform(g);
4294
+ g.setCoords();
4295
+ canvas.requestRenderAll();
4296
+ };
4297
+ const onMoving = (opt) => {
4298
+ const target = opt == null ? void 0 : opt.target;
4299
+ if (target !== g) return;
4300
+ const rawDx = (g.left ?? 0) - groupAnchor.left;
4301
+ const rawDy = (g.top ?? 0) - groupAnchor.top;
4302
+ const dx = rawDx - lastDragDx;
4303
+ const dy = rawDy - lastDragDy;
4304
+ lastDragDx = rawDx;
4305
+ lastDragDy = rawDy;
4306
+ g.left = groupAnchor.left;
4307
+ g.top = groupAnchor.top;
4308
+ if (dx === 0 && dy === 0) return;
4309
+ pendingDx += dx;
4310
+ pendingDy += dy;
4311
+ if (panRafId == null) panRafId = requestAnimationFrame(flushPan);
4312
+ };
4313
+ const onModified = (opt) => {
4314
+ const target = opt == null ? void 0 : opt.target;
4315
+ if (target !== g) return;
4316
+ if (panRafId != null) {
4317
+ cancelAnimationFrame(panRafId);
4318
+ panRafId = null;
4319
+ flushPan();
4320
+ }
4321
+ lastDragDx = 0;
4322
+ lastDragDy = 0;
4323
+ g.left = groupAnchor.left;
4324
+ g.top = groupAnchor.top;
4325
+ g.setCoords();
4326
+ };
4327
+ const onCanvasMouseDown = (opt) => {
4328
+ const target = opt.target;
4329
+ if (target === g || target === ghost || target === outline) return;
4330
+ setTimeout(() => exitCropMode(g, true), 0);
4331
+ };
4332
+ const onDblClick = (opt) => {
4333
+ const target = opt == null ? void 0 : opt.target;
4334
+ if (target !== g && target !== ghost && target !== outline) return;
4335
+ exitCropMode(g, true);
4336
+ };
4337
+ const onKeyDown = (e) => {
4338
+ if (e.key === "Escape" || e.key === "Enter") {
4339
+ e.preventDefault();
4340
+ e.stopPropagation();
4341
+ exitCropMode(g, true);
4342
+ }
4343
+ };
4344
+ let wheelCommitTimer = null;
4345
+ let zoomRafId = null;
4346
+ let pendingZoomDelta = 0;
4347
+ let pendingZoomCenter = null;
4348
+ let lastWheelWasPinch = false;
4349
+ const flushZoom = () => {
4350
+ zoomRafId = null;
4351
+ if (pendingZoomDelta === 0 || !pendingZoomCenter) return;
4352
+ const innerCt = g.__cropData;
4353
+ const img = innerCt == null ? void 0 : innerCt._img;
4354
+ if (!img) {
4355
+ pendingZoomDelta = 0;
4356
+ pendingZoomCenter = null;
4357
+ return;
4358
+ }
4359
+ const factor = lastWheelWasPinch ? 0.012 : 22e-4;
4360
+ const ratio = Math.exp(-pendingZoomDelta * factor);
4361
+ pendingZoomDelta = 0;
4362
+ img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
4363
+ const ct2 = img._ct;
4364
+ const prevZoom = ct2.zoom ?? 1;
4365
+ const nextZoom = clamp(prevZoom * ratio, 1, 8);
4366
+ if (nextZoom === prevZoom) {
4367
+ pendingZoomCenter = null;
4368
+ return;
4369
+ }
4370
+ const angle = g.angle ?? 0;
4371
+ const rad = fabric.util.degreesToRadians(-angle);
4372
+ const cos = Math.cos(rad);
4373
+ const sin = Math.sin(rad);
4374
+ const cx = g.left ?? 0;
4375
+ const cy = g.top ?? 0;
4376
+ const wx = pendingZoomCenter.x - cx;
4377
+ const wy = pendingZoomCenter.y - cy;
4378
+ const localCx = wx * cos - wy * sin;
4379
+ const localCy = wx * sin + wy * cos;
4380
+ const iw = img.width || 1;
4381
+ const ih = img.height || 1;
4382
+ const prevScale = img.scaleX || 1;
4383
+ const prevOverflowX = Math.max(0, iw * prevScale - innerCt.frameW);
4384
+ const prevOverflowY = Math.max(0, ih * prevScale - innerCt.frameH);
4385
+ const prevPanX = ct2.panX ?? 0.5;
4386
+ const prevPanY = ct2.panY ?? 0.5;
4387
+ const prevOffsetX = prevOverflowX > 0 ? -prevOverflowX * (prevPanX - 0.5) : 0;
4388
+ const prevOffsetY = prevOverflowY > 0 ? -prevOverflowY * (prevPanY - 0.5) : 0;
4389
+ const imgPxX = (localCx - prevOffsetX) / prevScale + iw / 2;
4390
+ const imgPxY = (localCy - prevOffsetY) / prevScale + ih / 2;
4391
+ ct2.zoom = nextZoom;
4392
+ updateCoverLayout(g);
4393
+ const newScale = img.scaleX || 1;
4394
+ const newOverflowX = Math.max(0, iw * newScale - innerCt.frameW);
4395
+ const newOverflowY = Math.max(0, ih * newScale - innerCt.frameH);
4396
+ if (newOverflowX > 0) {
4397
+ const desiredOffsetX = localCx - (imgPxX - iw / 2) * newScale;
4398
+ ct2.panX = clamp(0.5 - desiredOffsetX / newOverflowX, 0, 1);
4399
+ } else {
4400
+ ct2.panX = 0.5;
4401
+ }
4402
+ if (newOverflowY > 0) {
4403
+ const desiredOffsetY = localCy - (imgPxY - ih / 2) * newScale;
4404
+ ct2.panY = clamp(0.5 - desiredOffsetY / newOverflowY, 0, 1);
4405
+ } else {
4406
+ ct2.panY = 0.5;
4407
+ }
4408
+ updateCoverLayout(g);
4409
+ syncGhostTransform(g);
4410
+ g.setCoords();
4411
+ canvas.requestRenderAll();
4412
+ if (wheelCommitTimer) clearTimeout(wheelCommitTimer);
4413
+ wheelCommitTimer = setTimeout(() => {
4414
+ canvas.fire("object:modified", { target: g });
4415
+ g.left = groupAnchor.left;
4416
+ g.top = groupAnchor.top;
4417
+ g.setCoords();
4418
+ }, 200);
4419
+ pendingZoomCenter = null;
4420
+ };
4421
+ const onWheel = (e) => {
4422
+ if (!canvas) return;
4423
+ const innerCt = g.__cropData;
4424
+ if (!innerCt) return;
4425
+ const pointer = canvas.getPointer(e, false);
4426
+ const br = g.getBoundingRect();
4427
+ if (pointer.x < br.left || pointer.x > br.left + br.width || pointer.y < br.top || pointer.y > br.top + br.height) return;
4428
+ const img = innerCt._img;
4429
+ if (!img) return;
4430
+ e.preventDefault();
4431
+ e.stopPropagation();
4432
+ pendingZoomDelta += e.deltaY;
4433
+ pendingZoomCenter = pointer;
4434
+ lastWheelWasPinch = !!e.ctrlKey;
4435
+ if (zoomRafId == null) zoomRafId = requestAnimationFrame(flushZoom);
4436
+ };
4437
+ canvas.on("object:moving", onMoving);
4438
+ canvas.on("object:modified", onModified);
4439
+ canvas.on("mouse:down:before", onCanvasMouseDown);
4440
+ canvas.on("mouse:dblclick", onDblClick);
4441
+ window.addEventListener("keydown", onKeyDown, true);
4442
+ const upperCanvasEl = canvas.upperCanvasEl;
4443
+ upperCanvasEl == null ? void 0 : upperCanvasEl.addEventListener("wheel", onWheel, { passive: false });
4444
+ g[CROP_HANDLERS_FLAG] = {
4445
+ onMoving,
4446
+ onModified,
4447
+ onCanvasMouseDown,
4448
+ onDblClick,
4449
+ onKeyDown,
4450
+ onWheel,
4451
+ upperCanvasEl
4452
+ };
4453
+ installCropModeVisuals(g);
4454
+ canvas.setActiveObject(g);
4455
+ canvas.requestRenderAll();
4456
+ return true;
4457
+ }
4458
+ function exitCropMode(g, commit = true) {
4459
+ var _a;
4460
+ if (!g[CROP_MODE_FLAG]) return;
4461
+ const canvas = g.canvas;
4462
+ const handlers = g[CROP_HANDLERS_FLAG];
4463
+ if (handlers && canvas) {
4464
+ canvas.off("object:moving", handlers.onMoving);
4465
+ canvas.off("object:modified", handlers.onModified);
4466
+ canvas.off("mouse:down:before", handlers.onCanvasMouseDown);
4467
+ if (handlers.onDblClick) canvas.off("mouse:dblclick", handlers.onDblClick);
4468
+ window.removeEventListener("keydown", handlers.onKeyDown, true);
4469
+ (_a = handlers.upperCanvasEl) == null ? void 0 : _a.removeEventListener("wheel", handlers.onWheel);
4470
+ }
4471
+ g[CROP_HANDLERS_FLAG] = void 0;
4472
+ const ghost = g[CROP_GHOST_FLAG];
4473
+ const outline = g[CROP_OUTLINE_FLAG];
4474
+ if (canvas && ghost) canvas.remove(ghost);
4475
+ if (canvas && outline) canvas.remove(outline);
4476
+ g[CROP_GHOST_FLAG] = void 0;
4477
+ g[CROP_OUTLINE_FLAG] = void 0;
4478
+ g.lockMovementX = false;
4479
+ g.lockMovementY = false;
4480
+ g.lockRotation = false;
4481
+ g.lockScalingX = false;
4482
+ g.lockScalingY = false;
4483
+ g.hasControls = true;
4484
+ g.borderDashArray = void 0;
4485
+ installCanvaMaskControls(g);
4486
+ g.borderColor = void 0;
4487
+ g.cornerColor = void 0;
4488
+ g.cornerStrokeColor = void 0;
4489
+ g.cornerStyle = "rect";
4490
+ g[CROP_MODE_FLAG] = false;
4491
+ g.__cropZoomLastPointer = void 0;
4492
+ g.__lastPointerForCrop = void 0;
4493
+ if (commit && canvas) {
4494
+ canvas.fire("object:modified", { target: g });
4495
+ }
4496
+ canvas == null ? void 0 : canvas.requestRenderAll();
4497
+ }
3706
4498
  function getObjectSnapPoints(obj) {
3707
4499
  try {
3708
4500
  obj.setCoords();
@@ -5271,13 +6063,7 @@ const PageCanvas = forwardRef(
5271
6063
  const activeObj = fabricCanvas.getActiveObject();
5272
6064
  if (activeObj) applyControlSizeForZoom(fabricCanvas, activeObj);
5273
6065
  if (activeObj && !(activeObj instanceof fabric.ActiveSelection) && (((_a = activeObj._ct) == null ? void 0 : _a.isCropGroup) || activeObj.__cropGroup)) {
5274
- const objId = getObjectId(activeObj);
5275
- const element = elementsRef.current.find((el) => el.id === objId);
5276
- if (element == null ? void 0 : element.useCropHandles) {
5277
- installCanvaMaskControls(activeObj);
5278
- } else {
5279
- setSimpleScaleControls(activeObj);
5280
- }
6066
+ installCanvaMaskControls(activeObj);
5281
6067
  }
5282
6068
  });
5283
6069
  fabricCanvas.on("selection:updated", () => {
@@ -5286,13 +6072,7 @@ const PageCanvas = forwardRef(
5286
6072
  const activeObj = fabricCanvas.getActiveObject();
5287
6073
  if (activeObj) applyControlSizeForZoom(fabricCanvas, activeObj);
5288
6074
  if (activeObj && !(activeObj instanceof fabric.ActiveSelection) && (((_a = activeObj._ct) == null ? void 0 : _a.isCropGroup) || activeObj.__cropGroup)) {
5289
- const objId = getObjectId(activeObj);
5290
- const element = elementsRef.current.find((el) => el.id === objId);
5291
- if (element == null ? void 0 : element.useCropHandles) {
5292
- installCanvaMaskControls(activeObj);
5293
- } else {
5294
- setSimpleScaleControls(activeObj);
5295
- }
6075
+ installCanvaMaskControls(activeObj);
5296
6076
  }
5297
6077
  });
5298
6078
  fabricCanvas.on("selection:cleared", () => {
@@ -5310,6 +6090,8 @@ const PageCanvas = forwardRef(
5310
6090
  let dragStarted = false;
5311
6091
  const markTransforming = (target) => {
5312
6092
  if (!target) return;
6093
+ const targetId = getObjectId(target);
6094
+ if (targetId) transformingIdsRef.current.add(targetId);
5313
6095
  if (target instanceof fabric.Group) {
5314
6096
  target.getObjects().forEach((obj) => {
5315
6097
  const id2 = getObjectId(obj);
@@ -5330,7 +6112,7 @@ const PageCanvas = forwardRef(
5330
6112
  const clearTransforming = () => {
5331
6113
  transformingIdsRef.current.clear();
5332
6114
  };
5333
- const isCropGroup = (o) => {
6115
+ const isCropGroup2 = (o) => {
5334
6116
  var _a;
5335
6117
  return !!(((_a = o == null ? void 0 : o._ct) == null ? void 0 : _a.isCropGroup) || (o == null ? void 0 : o.__cropGroup));
5336
6118
  };
@@ -5338,9 +6120,9 @@ const PageCanvas = forwardRef(
5338
6120
  var _a, _b, _c;
5339
6121
  const t = opt.target;
5340
6122
  if (((_a = opt.e) == null ? void 0 : _a.type) !== "mousedown" && ((_b = opt.e) == null ? void 0 : _b.type) !== "pointerdown" && ((_c = opt.e) == null ? void 0 : _c.type) !== "touchstart") {
5341
- if (t && isCropGroup(t)) {
6123
+ if (t && isCropGroup2(t)) {
5342
6124
  fabricCanvas._hoveredTarget = t;
5343
- } else if ((t == null ? void 0 : t.group) && isCropGroup(t.group)) {
6125
+ } else if ((t == null ? void 0 : t.group) && isCropGroup2(t.group)) {
5344
6126
  fabricCanvas._hoveredTarget = t.group;
5345
6127
  }
5346
6128
  return;
@@ -5349,7 +6131,7 @@ const PageCanvas = forwardRef(
5349
6131
  const pointer = fabricCanvas.getPointer(opt.e);
5350
6132
  const objects = fabricCanvas.getObjects();
5351
6133
  for (const obj of objects) {
5352
- if (isCropGroup(obj) && obj.containsPoint(pointer)) {
6134
+ if (isCropGroup2(obj) && obj.containsPoint(pointer)) {
5353
6135
  fabricCanvas.setActiveObject(obj);
5354
6136
  opt.target = obj;
5355
6137
  fabricCanvas._hoveredTarget = obj;
@@ -5359,13 +6141,13 @@ const PageCanvas = forwardRef(
5359
6141
  return;
5360
6142
  }
5361
6143
  const g = t.group;
5362
- if (g && isCropGroup(g)) {
6144
+ if (g && isCropGroup2(g)) {
5363
6145
  fabricCanvas.setActiveObject(g);
5364
6146
  opt.target = g;
5365
6147
  fabricCanvas._hoveredTarget = g;
5366
6148
  return;
5367
6149
  }
5368
- if (isCropGroup(t)) {
6150
+ if (isCropGroup2(t)) {
5369
6151
  fabricCanvas.setActiveObject(t);
5370
6152
  t.set({
5371
6153
  selectable: true,
@@ -5908,8 +6690,8 @@ const PageCanvas = forwardRef(
5908
6690
  const h = (active.height ?? 0) * (active.scaleY ?? 1);
5909
6691
  const centerX = active.left ?? 0;
5910
6692
  const centerY = active.top ?? 0;
5911
- const groupLeft = centerX - w / 2;
5912
- const groupTop = centerY - h / 2;
6693
+ const groupLeft = active.originX === "center" ? centerX - w / 2 : centerX;
6694
+ const groupTop = active.originY === "center" ? centerY - h / 2 : centerY;
5913
6695
  const storePos = absoluteToStorePosition(groupLeft, groupTop, groupId, pageChildren2);
5914
6696
  useEditorStore.getState().updateNode(groupId, { left: storePos.left, top: storePos.top }, { recordHistory: false, skipLayoutRecalc: true });
5915
6697
  commitHistory();
@@ -5942,76 +6724,79 @@ const PageCanvas = forwardRef(
5942
6724
  const commonAncestor = findCommonAncestorGroup(selectedElementIds, pageChildren2);
5943
6725
  const candidateGroup = sameDirectParent ? parentGroups[0] : commonAncestor;
5944
6726
  const candidateIsStack = candidateGroup && isStackLayoutMode(candidateGroup.layoutMode);
5945
- const groupToMove = candidateIsStack ? candidateGroup : null;
5946
- if (groupToMove && firstId) {
5947
- const firstNode = findNodeById(pageChildren2, firstId);
5948
- if (firstNode) {
5949
- const origAbs = getAbsoluteBounds(firstNode, pageChildren2);
5950
- let absoluteLeft;
5951
- let absoluteTop;
5952
- if (isActiveSelection && activeObj) {
5953
- const selectionMatrix = activeObj.calcTransformMatrix();
5954
- const relativePoint = { x: firstObj.left ?? 0, y: firstObj.top ?? 0 };
5955
- const absolutePoint = fabric.util.transformPoint(relativePoint, selectionMatrix);
5956
- absoluteLeft = absolutePoint.x;
5957
- absoluteTop = absolutePoint.y;
5958
- } else {
5959
- absoluteLeft = firstObj.left ?? 0;
5960
- absoluteTop = firstObj.top ?? 0;
6727
+ const allMembersSelected = (() => {
6728
+ if (!candidateGroup) return false;
6729
+ const memberIds = getAllElementIds(candidateGroup.children ?? []);
6730
+ if (memberIds.length === 0) return false;
6731
+ const selectedSet = new Set(selectedElementIds);
6732
+ return memberIds.every((mid) => selectedSet.has(mid));
6733
+ })();
6734
+ const groupToMove = candidateIsStack || allMembersSelected ? candidateGroup : null;
6735
+ if (groupToMove) {
6736
+ const groupAbs = getAbsoluteBounds(groupToMove, pageChildren2);
6737
+ let movedGroupLeft = groupAbs.left;
6738
+ let movedGroupTop = groupAbs.top;
6739
+ if (activeObj) {
6740
+ const selectionRect = activeObj.getBoundingRect();
6741
+ movedGroupLeft = selectionRect.left;
6742
+ movedGroupTop = selectionRect.top;
6743
+ } else if (firstId) {
6744
+ const firstNode = findNodeById(pageChildren2, firstId);
6745
+ if (firstNode) {
6746
+ movedGroupLeft = getAbsoluteBounds(firstNode, pageChildren2).left;
6747
+ movedGroupTop = getAbsoluteBounds(firstNode, pageChildren2).top;
5961
6748
  }
5962
- const deltaX = absoluteLeft - origAbs.left;
5963
- const deltaY = absoluteTop - origAbs.top;
5964
- const hadScale = isActiveSelection && activeObj && (Math.abs((activeObj.scaleX ?? 1) - 1) > 0.01 || Math.abs((activeObj.scaleY ?? 1) - 1) > 0.01);
5965
- if (!hadScale && (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1)) {
5966
- if (typeof console !== "undefined" && console.log) {
5967
- console.log("[object:modified] plain-groups: moving group id=", groupToMove.id, "newLeft=", (groupToMove.left ?? 0) + deltaX, "newTop=", (groupToMove.top ?? 0) + deltaY);
5968
- }
5969
- const { updateNode: updateNodeStore, commitHistory: commitHistoryStore, getCurrentElements } = useEditorStore.getState();
5970
- const newLeft = (groupToMove.left ?? 0) + deltaX;
5971
- const newTop = (groupToMove.top ?? 0) + deltaY;
5972
- updateNodeStore(groupToMove.id, { left: newLeft, top: newTop }, { recordHistory: false, skipLayoutRecalc: true });
5973
- commitHistoryStore();
5974
- if (isActiveSelection && activeObj instanceof fabric.ActiveSelection) {
5975
- skipSelectionClearOnDiscardRef.current = true;
5976
- fabricCanvas.discardActiveObject();
5977
- }
5978
- const stateAfter = useEditorStore.getState();
5979
- const pageAfter = stateAfter.canvas.pages.find((p) => p.id === pageId);
5980
- const pageChildrenAfter = (pageAfter == null ? void 0 : pageAfter.children) ?? [];
5981
- for (const obj of activeObjects) {
5982
- const objId = getObjectId(obj);
5983
- if (!objId || objId === "__background__") continue;
5984
- const node = findNodeById(pageChildrenAfter, objId);
5985
- if (node) {
5986
- const abs = getAbsoluteBounds(node, pageChildrenAfter);
5987
- if (obj instanceof fabric.Group && obj.__cropGroup) {
5988
- const w = (obj.width ?? 0) * (obj.scaleX ?? 1);
5989
- const h = (obj.height ?? 0) * (obj.scaleY ?? 1);
5990
- obj.set({ left: abs.left + w / 2, top: abs.top + h / 2 });
5991
- } else {
5992
- obj.set({ left: abs.left, top: abs.top });
5993
- }
5994
- obj.setCoords();
5995
- }
5996
- }
5997
- if (isActiveSelection && activeObjects.length > 0) {
5998
- const selection = new fabric.ActiveSelection([...activeObjects], { canvas: fabricCanvas });
5999
- fabricCanvas.setActiveObject(selection);
6000
- skipSelectionClearOnDiscardRef.current = false;
6749
+ }
6750
+ const deltaX = movedGroupLeft - groupAbs.left;
6751
+ const deltaY = movedGroupTop - groupAbs.top;
6752
+ const hadScale = isActiveSelection && activeObj && (Math.abs((activeObj.scaleX ?? 1) - 1) > 0.01 || Math.abs((activeObj.scaleY ?? 1) - 1) > 0.01);
6753
+ if (!hadScale && (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1)) {
6754
+ if (typeof console !== "undefined" && console.log) {
6755
+ console.log("[object:modified] plain-groups: moving group id=", groupToMove.id, "newLeft=", (groupToMove.left ?? 0) + deltaX, "newTop=", (groupToMove.top ?? 0) + deltaY);
6756
+ }
6757
+ const { updateNode: updateNodeStore, commitHistory: commitHistoryStore, getCurrentElements } = useEditorStore.getState();
6758
+ const newLeft = (groupToMove.left ?? 0) + deltaX;
6759
+ const newTop = (groupToMove.top ?? 0) + deltaY;
6760
+ updateNodeStore(groupToMove.id, { left: newLeft, top: newTop }, { recordHistory: false, skipLayoutRecalc: true });
6761
+ commitHistoryStore();
6762
+ if (isActiveSelection && activeObj instanceof fabric.ActiveSelection) {
6763
+ skipSelectionClearOnDiscardRef.current = true;
6764
+ fabricCanvas.discardActiveObject();
6765
+ }
6766
+ const stateAfter = useEditorStore.getState();
6767
+ const pageAfter = stateAfter.canvas.pages.find((p) => p.id === pageId);
6768
+ const pageChildrenAfter = (pageAfter == null ? void 0 : pageAfter.children) ?? [];
6769
+ for (const obj of activeObjects) {
6770
+ const objId = getObjectId(obj);
6771
+ if (!objId || objId === "__background__") continue;
6772
+ const node = findNodeById(pageChildrenAfter, objId);
6773
+ if (node) {
6774
+ const abs = getAbsoluteBounds(node, pageChildrenAfter);
6775
+ const w = (obj.width ?? 0) * (obj.scaleX ?? 1);
6776
+ const h = (obj.height ?? 0) * (obj.scaleY ?? 1);
6777
+ const nextLeft = obj.originX === "center" ? abs.left + w / 2 : abs.left;
6778
+ const nextTop = obj.originY === "center" ? abs.top + h / 2 : abs.top;
6779
+ obj.set({ left: nextLeft, top: nextTop });
6780
+ obj.setCoords();
6001
6781
  }
6002
- fabricCanvas.requestRenderAll();
6003
- elementsRef.current = getCurrentElements();
6004
- for (const obj of activeObjects) {
6005
- const objId = getObjectId(obj);
6006
- if (objId && objId !== "__background__") {
6007
- justModifiedIdsRef.current.add(objId);
6008
- modifiedIdsThisRound.add(objId);
6009
- }
6782
+ }
6783
+ if (isActiveSelection && activeObjects.length > 0) {
6784
+ const selection = new fabric.ActiveSelection([...activeObjects], { canvas: fabricCanvas });
6785
+ fabricCanvas.setActiveObject(selection);
6786
+ skipSelectionClearOnDiscardRef.current = false;
6787
+ }
6788
+ fabricCanvas.requestRenderAll();
6789
+ elementsRef.current = getCurrentElements();
6790
+ for (const obj of activeObjects) {
6791
+ const objId = getObjectId(obj);
6792
+ if (objId && objId !== "__background__") {
6793
+ justModifiedIdsRef.current.add(objId);
6794
+ modifiedIdsThisRound.add(objId);
6010
6795
  }
6011
- setTimeout(() => modifiedIdsThisRound.forEach((id) => justModifiedIdsRef.current.delete(id)), 150);
6012
- unlockEditsSoon();
6013
- return;
6014
6796
  }
6797
+ setTimeout(() => modifiedIdsThisRound.forEach((id) => justModifiedIdsRef.current.delete(id)), 150);
6798
+ unlockEditsSoon();
6799
+ return;
6015
6800
  }
6016
6801
  }
6017
6802
  }
@@ -6090,9 +6875,13 @@ const PageCanvas = forwardRef(
6090
6875
  finalScaleX = 1;
6091
6876
  finalScaleY = 1;
6092
6877
  obj.set({ scaleX: 1, scaleY: 1 });
6093
- updateCoverLayout(obj);
6094
- obj.__lastResizeHandle = null;
6095
- fabricCanvas.setActiveObject(obj);
6878
+ if (!isActiveSelection) {
6879
+ updateCoverLayout(obj);
6880
+ obj.__lastResizeHandle = null;
6881
+ fabricCanvas.setActiveObject(obj);
6882
+ } else {
6883
+ obj.__lastResizeHandle = null;
6884
+ }
6096
6885
  }
6097
6886
  } else if (obj instanceof fabric.FabricImage) {
6098
6887
  if ((sourceElement == null ? void 0 : sourceElement.smartElementType) && sourceElement.smartProps) {
@@ -6235,6 +7024,13 @@ const PageCanvas = forwardRef(
6235
7024
  fabricCanvas.on("mouse:dblclick", (e) => {
6236
7025
  if (!isActiveRef.current || !allowEditing) return;
6237
7026
  let target = e.target;
7027
+ if (target && target instanceof fabric.Group && target.__cropGroup) {
7028
+ const ct = target.__cropData;
7029
+ if ((ct == null ? void 0 : ct._img) && !isCropGroupInCropMode(target)) {
7030
+ enterCropMode(target);
7031
+ return;
7032
+ }
7033
+ }
6238
7034
  if (!target || !(target instanceof fabric.Textbox)) {
6239
7035
  const active = fabricCanvas.getActiveObject();
6240
7036
  if (active instanceof fabric.Textbox) target = active;
@@ -6427,9 +7223,9 @@ const PageCanvas = forwardRef(
6427
7223
  const activeId = activeBeforeSync ? getObjectId(activeBeforeSync) : null;
6428
7224
  const isActiveTextBeingEdited = activeId && editingTextIdRef.current === activeId && activeBeforeSync instanceof fabric.Textbox;
6429
7225
  if (!skipRestoreSelection && activeBeforeSync && activeStillOnCanvas && !isActiveTextBeingEdited) {
6430
- const isCropGroup = ((_d = activeBeforeSync._ct) == null ? void 0 : _d.isCropGroup) || activeBeforeSync.__cropGroup;
7226
+ const isCropGroup2 = ((_d = activeBeforeSync._ct) == null ? void 0 : _d.isCropGroup) || activeBeforeSync.__cropGroup;
6431
7227
  const isSectionGroup = activeId && sectionGroupIds.has(activeId);
6432
- if ((isCropGroup || !shouldSkipUpdates2) && !isSectionGroup) {
7228
+ if ((isCropGroup2 || !shouldSkipUpdates2) && !isSectionGroup) {
6433
7229
  fc.setActiveObject(activeBeforeSync);
6434
7230
  }
6435
7231
  }
@@ -6550,11 +7346,12 @@ const PageCanvas = forwardRef(
6550
7346
  const sourceUrlChanged = currentUrlNormalized !== storedUrlNormalized;
6551
7347
  const needsReload = sourceUrlChanged || colorMapChanged;
6552
7348
  const hasUrl = currentUrlNormalized !== "";
6553
- const isCropGroup = existingObj instanceof fabric.Group && existingObj.__cropGroup;
6554
- const isPlaceholder = existingObj instanceof fabric.Group && !isCropGroup || !(existingObj instanceof fabric.FabricImage);
7349
+ const isCropGroup2 = existingObj instanceof fabric.Group && existingObj.__cropGroup;
7350
+ const isPlaceholderGroup = isEmptyImagePlaceholderGroup(existingObj);
7351
+ const isPlaceholder = isPlaceholderGroup || !(existingObj instanceof fabric.FabricImage) || existingObj instanceof fabric.Group && !isCropGroup2;
6555
7352
  const hadUrlBefore = storedImageUrl && String(storedImageUrl).trim() !== "";
6556
7353
  if (!hasUrl && hadUrlBefore) {
6557
- const placeholder = isCropGroup ? createImagePlaceholderForGroup(element) : createImagePlaceholder(element);
7354
+ const placeholder = isCropGroup2 ? createImagePlaceholderForGroup(element) : createImagePlaceholder(element);
6558
7355
  setObjectData(placeholder, element.id);
6559
7356
  placeholder.__imageSrc = "";
6560
7357
  const idx = fc.getObjects().indexOf(existingObj);
@@ -6570,13 +7367,13 @@ const PageCanvas = forwardRef(
6570
7367
  const imageFitForReplace = element.imageFit || ((_e = element.style) == null ? void 0 : _e.imageFit) || "cover";
6571
7368
  const clipShapeForReplace = element.clipShape ?? ((_f = element.style) == null ? void 0 : _f.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
6572
7369
  const needCropGroupForElement = imageFitForReplace !== "fill" || clipShapeForReplace && clipShapeForReplace !== "none";
6573
- const plainImageNeedsCropGroup = hasUrl && !isCropGroup && existingObj instanceof fabric.FabricImage && needCropGroupForElement;
7370
+ const plainImageNeedsCropGroup = hasUrl && !isCropGroup2 && existingObj instanceof fabric.FabricImage && needCropGroupForElement;
6574
7371
  if (hasUrl && (needsReload || isPlaceholder || plainImageNeedsCropGroup)) {
6575
7372
  if (needsReload && !isBeingTransformed && (!wasJustModified || sourceUrlChanged)) {
6576
7373
  loadImageAsync(element, existingObj, fc);
6577
7374
  } else if (plainImageNeedsCropGroup) {
6578
7375
  loadImageAsync(element, existingObj, fc);
6579
- } else if (!needsReload && isCropGroup) {
7376
+ } else if (!needsReload && isCropGroup2) {
6580
7377
  const ct = existingObj.__cropData;
6581
7378
  if (ct) {
6582
7379
  const resolvedCrop = pageTree.length > 0 ? getNodeBounds(element, pageTree) : { width: typeof element.width === "number" ? element.width : 200, height: typeof element.height === "number" ? element.height : 50 };
@@ -6622,6 +7419,22 @@ const PageCanvas = forwardRef(
6622
7419
  existingObj.set({ opacity: clampedOpacity });
6623
7420
  existingObj.set({ width: ct.frameW, height: ct.frameH });
6624
7421
  existingObj.set({ backgroundColor: "transparent" });
7422
+ const liveMask = existingObj.clipPath;
7423
+ const liveMaskUrl = existingObj.__svgMaskUrl;
7424
+ const liveMaskType = existingObj.__svgMaskType;
7425
+ const schemaMaskUrl = element.maskSvgUrl;
7426
+ const schemaMaskType = element.maskType || "shape";
7427
+ if (liveMask && liveMask.__svgMask && !schemaMaskUrl) {
7428
+ existingObj.clipPath = void 0;
7429
+ delete existingObj.__svgMaskUrl;
7430
+ delete existingObj.__svgMaskType;
7431
+ existingObj.dirty = true;
7432
+ } else if (schemaMaskUrl && (schemaMaskUrl !== liveMaskUrl || schemaMaskType !== liveMaskType)) {
7433
+ Promise.resolve().then(() => svgMaskApply).then(({ applyMaskToCropGroup: applyMaskToCropGroup2 }) => {
7434
+ applyMaskToCropGroup2(existingObj, schemaMaskUrl, schemaMaskType).catch(() => {
7435
+ });
7436
+ });
7437
+ }
6625
7438
  if (shapeChanged) {
6626
7439
  const needsNewClipPath = !existingObj.clipPath || newShape === "circle" && !(existingObj.clipPath instanceof fabric.Ellipse) || newShape !== "circle" && !(existingObj.clipPath instanceof fabric.Rect);
6627
7440
  if (needsNewClipPath) {
@@ -6658,11 +7471,7 @@ const PageCanvas = forwardRef(
6658
7471
  }
6659
7472
  updateCoverLayout(existingObj);
6660
7473
  if (allowEditing) {
6661
- if (element.useCropHandles) {
6662
- installCanvaMaskControls(existingObj);
6663
- } else {
6664
- setSimpleScaleControls(existingObj);
6665
- }
7474
+ installCanvaMaskControls(existingObj);
6666
7475
  } else {
6667
7476
  existingObj.set({
6668
7477
  hasControls: false,
@@ -6711,6 +7520,45 @@ const PageCanvas = forwardRef(
6711
7520
  }
6712
7521
  placeholder.__imageSrc = void 0;
6713
7522
  } else if (!isBeingTransformed && !isBeingTextEdited && !shouldSkipUpdates2) {
7523
+ if (isPlaceholderGroup) {
7524
+ const storePosImg = pageChildren ? (() => {
7525
+ const node = findNodeById(pageChildren, element.id);
7526
+ return node ? getAbsoluteBounds(node, pageChildren) : { left: element.left ?? 0, top: element.top ?? 0 };
7527
+ })() : { left: element.left ?? 0, top: element.top ?? 0 };
7528
+ const resolvedSizeImg = (pageChildren == null ? void 0 : pageChildren.length) ? getNodeBounds(element, pageChildren) : { width: typeof element.width === "number" ? element.width : 200, height: typeof element.height === "number" ? element.height : 50 };
7529
+ const hasExplicitSize = typeof element.width === "number" && Number.isFinite(element.width) && element.width > 0 && typeof element.height === "number" && Number.isFinite(element.height) && element.height > 0;
7530
+ const minVisiblePlaceholder = hasExplicitSize ? 1 : 20;
7531
+ const nextWidth = Math.max(minVisiblePlaceholder, Number(resolvedSizeImg.width) || 200);
7532
+ const nextHeight = Math.max(minVisiblePlaceholder, Number(resolvedSizeImg.height) || 50);
7533
+ const isDynamicField = dynamicFieldIds.includes(element.id);
7534
+ const canBeEvented = isEditorMode || isPreviewMode && isDynamicField;
7535
+ updateEmptyPlaceholderLayout(existingObj, nextWidth, nextHeight);
7536
+ existingObj.set({
7537
+ left: storePosImg.left + nextWidth / 2,
7538
+ top: storePosImg.top + nextHeight / 2,
7539
+ originX: "center",
7540
+ originY: "center",
7541
+ angle: element.angle ?? 0,
7542
+ opacity: isHidden ? 0 : element.opacity ?? 1,
7543
+ flipX: element.flipX ?? false,
7544
+ flipY: element.flipY ?? false,
7545
+ selectable: allowSelection && !isHidden,
7546
+ evented: canBeEvented && !isHidden,
7547
+ hasControls: allowEditing && !isHidden,
7548
+ hasBorders: allowEditing && !isHidden,
7549
+ interactive: allowEditing && !isHidden,
7550
+ subTargetCheck: false,
7551
+ lockMovementX: !allowSelection,
7552
+ lockMovementY: !allowSelection,
7553
+ hoverCursor: isDynamicField && isPreviewMode ? "pointer" : void 0
7554
+ });
7555
+ if (allowEditing) {
7556
+ installCanvaMaskControls(existingObj);
7557
+ }
7558
+ existingObj.setCoords();
7559
+ fc.requestRenderAll();
7560
+ continue;
7561
+ }
6714
7562
  if (existingObj instanceof fabric.Group && existingObj.__cropGroup) {
6715
7563
  existingObj.set({
6716
7564
  flipX: element.flipX ?? false,
@@ -6885,7 +7733,14 @@ const PageCanvas = forwardRef(
6885
7733
  const node = findNodeById(pageTree, element.id);
6886
7734
  return node ? getAbsoluteBounds(node, pageTree) : { left: element.left ?? 0, top: element.top ?? 0 };
6887
7735
  })() : { left: element.left ?? 0, top: element.top ?? 0 };
6888
- placeholder.set({ left: absPosImg.left, top: absPosImg.top });
7736
+ const placeholderWidth = Number((placeholder.width ?? 0) * (placeholder.scaleX ?? 1));
7737
+ const placeholderHeight = Number((placeholder.height ?? 0) * (placeholder.scaleY ?? 1));
7738
+ placeholder.set({
7739
+ left: absPosImg.left + placeholderWidth / 2,
7740
+ top: absPosImg.top + placeholderHeight / 2,
7741
+ originX: "center",
7742
+ originY: "center"
7743
+ });
6889
7744
  placeholder.setCoords();
6890
7745
  const imageUrl = element.src || element.imageUrl;
6891
7746
  placeholder.__imageSrc = imageUrl;
@@ -6978,8 +7833,8 @@ const PageCanvas = forwardRef(
6978
7833
  isRebuildingRef.current = false;
6979
7834
  fc.requestRenderAll();
6980
7835
  if (activeBeforeSync && fc.getObjects().includes(activeBeforeSync)) {
6981
- const isCropGroup = ((_i = activeBeforeSync._ct) == null ? void 0 : _i.isCropGroup) || activeBeforeSync.__cropGroup;
6982
- if (isCropGroup) {
7836
+ const isCropGroup2 = ((_i = activeBeforeSync._ct) == null ? void 0 : _i.isCropGroup) || activeBeforeSync.__cropGroup;
7837
+ if (isCropGroup2) {
6983
7838
  fc.setActiveObject(activeBeforeSync);
6984
7839
  fc.requestRenderAll();
6985
7840
  }
@@ -7406,11 +8261,7 @@ const PageCanvas = forwardRef(
7406
8261
  obj.clipPath.dirty = true;
7407
8262
  obj.clipPath.setCoords();
7408
8263
  }
7409
- if (element == null ? void 0 : element.useCropHandles) {
7410
- installCanvaMaskControls(obj);
7411
- } else {
7412
- setSimpleScaleControls(obj);
7413
- }
8264
+ installCanvaMaskControls(obj);
7414
8265
  obj.set({
7415
8266
  selectable: true,
7416
8267
  evented: true,
@@ -8185,11 +9036,7 @@ const PageCanvas = forwardRef(
8185
9036
  evented: canBeEvented && !isHidden
8186
9037
  });
8187
9038
  } else {
8188
- if (element.useCropHandles) {
8189
- installCanvaMaskControls(cropGroup);
8190
- } else {
8191
- setSimpleScaleControls(cropGroup);
8192
- }
9039
+ installCanvaMaskControls(cropGroup);
8193
9040
  }
8194
9041
  const cropImg = (_l = cropGroup.__cropData) == null ? void 0 : _l._img;
8195
9042
  if (cropImg) {
@@ -8209,6 +9056,16 @@ const PageCanvas = forwardRef(
8209
9056
  useEditorStore.getState().updateElement(element.id, { imageNaturalWidth: nw, imageNaturalHeight: nh }, { recordHistory: false });
8210
9057
  }
8211
9058
  }
9059
+ const persistedMaskUrl = element.maskSvgUrl;
9060
+ if (persistedMaskUrl && typeof persistedMaskUrl === "string") {
9061
+ try {
9062
+ const { applyMaskToCropGroup: applyMaskToCropGroup2 } = await Promise.resolve().then(() => svgMaskApply);
9063
+ const persistedMaskType = element.maskType || "shape";
9064
+ await applyMaskToCropGroup2(cropGroup, persistedMaskUrl, persistedMaskType);
9065
+ } catch (err) {
9066
+ console.warn("[mask] failed to re-apply persisted mask", err);
9067
+ }
9068
+ }
8212
9069
  finalObject = cropGroup;
8213
9070
  } else {
8214
9071
  const isHiddenFill = !element.visible;
@@ -10497,11 +11354,11 @@ async function resolveTemplateData(options) {
10497
11354
  }
10498
11355
  async function resolveFromForm(options) {
10499
11356
  var _a, _b, _c;
10500
- const { templateId, formSchemaId, sectionState, themeId, supabaseUrl, supabaseAnonKey } = options;
11357
+ const { templateId, formSchemaId, sectionState, themeId, supabaseUrl, supabaseAnonKey, prefetched } = options;
10501
11358
  const [templateRow, formSchemaRow, defaultForm] = await Promise.all([
10502
- fetchRow(supabaseUrl, supabaseAnonKey, "templates", templateId),
10503
- fetchRow(supabaseUrl, supabaseAnonKey, "form_schemas", formSchemaId),
10504
- fetchDefaultForm(supabaseUrl, supabaseAnonKey, formSchemaId)
11359
+ (prefetched == null ? void 0 : prefetched.templateRow) ? Promise.resolve(prefetched.templateRow) : fetchRow(supabaseUrl, supabaseAnonKey, "templates", templateId),
11360
+ (prefetched == null ? void 0 : prefetched.formSchemaRow) !== void 0 ? Promise.resolve(prefetched.formSchemaRow) : fetchRow(supabaseUrl, supabaseAnonKey, "form_schemas", formSchemaId),
11361
+ (prefetched == null ? void 0 : prefetched.defaultForm) !== void 0 ? Promise.resolve(prefetched.defaultForm) : fetchDefaultForm(supabaseUrl, supabaseAnonKey, formSchemaId)
10505
11362
  ]);
10506
11363
  const templateConfig = templateRow.config;
10507
11364
  const templateFormSchema = templateRow.form_schema;
@@ -11114,14 +11971,15 @@ class PixldocsRenderer {
11114
11971
  * This is the primary external API for the package.
11115
11972
  */
11116
11973
  async renderFromForm(options) {
11117
- const { templateId, formSchemaId, sectionState, themeId, watermark, ...renderOpts } = options;
11974
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched, ...renderOpts } = options;
11118
11975
  const resolved = await resolveFromForm({
11119
11976
  templateId,
11120
11977
  formSchemaId,
11121
11978
  sectionState,
11122
11979
  themeId,
11123
11980
  supabaseUrl: this.config.supabaseUrl,
11124
- supabaseAnonKey: this.config.supabaseAnonKey
11981
+ supabaseAnonKey: this.config.supabaseAnonKey,
11982
+ prefetched
11125
11983
  });
11126
11984
  const shouldWatermark = watermark ?? resolved.price > 0;
11127
11985
  let configToRender = resolved.config;
@@ -11166,14 +12024,15 @@ class PixldocsRenderer {
11166
12024
  * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
11167
12025
  */
11168
12026
  async renderSvgsFromForm(options) {
11169
- const { templateId, formSchemaId, sectionState, themeId, watermark } = options;
12027
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched } = options;
11170
12028
  const resolved = await resolveFromForm({
11171
12029
  templateId,
11172
12030
  formSchemaId,
11173
12031
  sectionState,
11174
12032
  themeId,
11175
12033
  supabaseUrl: this.config.supabaseUrl,
11176
- supabaseAnonKey: this.config.supabaseAnonKey
12034
+ supabaseAnonKey: this.config.supabaseAnonKey,
12035
+ prefetched
11177
12036
  });
11178
12037
  const shouldWatermark = watermark ?? resolved.price > 0;
11179
12038
  let configToRender = resolved.config;
@@ -11197,14 +12056,15 @@ class PixldocsRenderer {
11197
12056
  * This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
11198
12057
  */
11199
12058
  async renderPdfFromForm(options) {
11200
- const { templateId, formSchemaId, sectionState, themeId, watermark, title, fontBaseUrl } = options;
12059
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched, title, fontBaseUrl } = options;
11201
12060
  const resolved = await resolveFromForm({
11202
12061
  templateId,
11203
12062
  formSchemaId,
11204
12063
  sectionState,
11205
12064
  themeId,
11206
12065
  supabaseUrl: this.config.supabaseUrl,
11207
- supabaseAnonKey: this.config.supabaseAnonKey
12066
+ supabaseAnonKey: this.config.supabaseAnonKey,
12067
+ prefetched
11208
12068
  });
11209
12069
  const shouldWatermark = watermark ?? resolved.price > 0;
11210
12070
  let configToRender = resolved.config;