@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.cjs CHANGED
@@ -2951,12 +2951,117 @@ async function normalizeSvgImageDimensions(fabricImage, imageUrl, sourceFormat)
2951
2951
  fabricImage.setCoords();
2952
2952
  }
2953
2953
  }
2954
+ const EMPTY_IMAGE_PLACEHOLDER_URL = "/image-placeholder.png";
2955
+ let placeholderTileImage = null;
2956
+ let placeholderTilePromise = null;
2957
+ function loadPlaceholderTile() {
2958
+ if (placeholderTileImage) return Promise.resolve(placeholderTileImage);
2959
+ if (placeholderTilePromise) return placeholderTilePromise;
2960
+ placeholderTilePromise = new Promise((resolve, reject) => {
2961
+ const img = new Image();
2962
+ img.crossOrigin = "anonymous";
2963
+ img.onload = () => {
2964
+ placeholderTileImage = img;
2965
+ resolve(img);
2966
+ };
2967
+ img.onerror = (e) => {
2968
+ placeholderTilePromise = null;
2969
+ reject(e);
2970
+ };
2971
+ img.src = EMPTY_IMAGE_PLACEHOLDER_URL;
2972
+ });
2973
+ return placeholderTilePromise;
2974
+ }
2975
+ function isEmptyImagePlaceholderGroup(obj) {
2976
+ return obj instanceof fabric__namespace.Group && Boolean(obj.__emptyImagePlaceholder);
2977
+ }
2978
+ function stabilizePlaceholderGroup(group, width, height) {
2979
+ group.set({ width, height, scaleX: 1, scaleY: 1 });
2980
+ if (group.layoutManager) {
2981
+ if (typeof fabric__namespace.FixedLayout === "function") {
2982
+ group.layoutManager.strategy = new fabric__namespace.FixedLayout();
2983
+ } else {
2984
+ group.layoutManager.performLayout = () => {
2985
+ };
2986
+ }
2987
+ }
2988
+ group.setCoords();
2989
+ }
2990
+ function attachEmptyPlaceholderImage(group, width, height) {
2991
+ loadPlaceholderTile().then((htmlImg) => {
2992
+ var _a;
2993
+ const objects = group._objects;
2994
+ const bgRect = objects == null ? void 0 : objects.find((obj) => obj.__isPlaceholderFrame);
2995
+ if (!bgRect) return;
2996
+ const pattern = new fabric__namespace.Pattern({
2997
+ source: htmlImg,
2998
+ repeat: "repeat"
2999
+ });
3000
+ pattern.__isPlaceholderPattern = true;
3001
+ bgRect.set({ fill: pattern });
3002
+ bgRect.dirty = true;
3003
+ if (!group.clipPath) {
3004
+ const clip = new fabric__namespace.Rect({
3005
+ width,
3006
+ height,
3007
+ left: 0,
3008
+ top: 0,
3009
+ originX: "center",
3010
+ originY: "center",
3011
+ selectable: false,
3012
+ evented: false
3013
+ });
3014
+ clip.absolutePositioned = false;
3015
+ clip.excludeFromExport = true;
3016
+ group.clipPath = clip;
3017
+ }
3018
+ stabilizePlaceholderGroup(group, width, height);
3019
+ group.dirty = true;
3020
+ (_a = group.canvas) == null ? void 0 : _a.requestRenderAll();
3021
+ }).catch(() => {
3022
+ });
3023
+ }
3024
+ function updateEmptyPlaceholderLayout(group, width, height) {
3025
+ const objects = group._objects;
3026
+ const backgroundRect = objects == null ? void 0 : objects.find((obj) => obj.__isPlaceholderFrame);
3027
+ const cropData = group.__cropData;
3028
+ if (cropData) {
3029
+ cropData.frameW = width;
3030
+ cropData.frameH = height;
3031
+ }
3032
+ backgroundRect == null ? void 0 : backgroundRect.set({
3033
+ width,
3034
+ height,
3035
+ left: 0,
3036
+ top: 0,
3037
+ originX: "center",
3038
+ originY: "center"
3039
+ });
3040
+ if (backgroundRect) backgroundRect.dirty = true;
3041
+ if (backgroundRect && !(backgroundRect.fill instanceof fabric__namespace.Pattern)) {
3042
+ attachEmptyPlaceholderImage(group, width, height);
3043
+ }
3044
+ if (group.clipPath && (group.clipPath instanceof fabric__namespace.Rect || group.clipPath instanceof fabric__namespace.Ellipse)) {
3045
+ group.clipPath.set({
3046
+ left: 0,
3047
+ top: 0,
3048
+ originX: "center",
3049
+ originY: "center",
3050
+ ...group.clipPath instanceof fabric__namespace.Ellipse ? { rx: width / 2, ry: height / 2 } : { width, height }
3051
+ });
3052
+ group.clipPath.setCoords();
3053
+ group.clipPath.dirty = true;
3054
+ }
3055
+ stabilizePlaceholderGroup(group, width, height);
3056
+ group.dirty = true;
3057
+ }
2954
3058
  function createImagePlaceholder(element) {
2955
3059
  const visualWidth = Number(element.width) * (element.scaleX ?? 1);
2956
3060
  const visualHeight = Number(element.height) * (element.scaleY ?? 1);
3061
+ const hasImage = !!(element.src || element.imageUrl);
2957
3062
  const bgRect = new fabric__namespace.Rect({
2958
- originX: "left",
2959
- originY: "top",
3063
+ originX: "center",
3064
+ originY: "center",
2960
3065
  left: 0,
2961
3066
  top: 0,
2962
3067
  width: visualWidth,
@@ -2967,56 +3072,65 @@ function createImagePlaceholder(element) {
2967
3072
  rx: element.clipShape === "rounded" ? element.rx || 8 : 0,
2968
3073
  ry: element.clipShape === "rounded" ? element.ry || 8 : 0
2969
3074
  });
3075
+ bgRect.__isPlaceholderFrame = true;
2970
3076
  const group = new fabric__namespace.Group([bgRect], {
2971
- left: element.left,
2972
- top: element.top,
2973
- originX: "left",
2974
- originY: "top",
2975
- selectable: element.selectable,
2976
- evented: element.evented
2977
- });
2978
- return group;
2979
- }
2980
- function createImagePlaceholderForGroup(element) {
2981
- const frameWidth = Number(element.width) * (element.scaleX ?? 1);
2982
- const frameHeight = Number(element.height) * (element.scaleY ?? 1);
2983
- const bgRect = new fabric__namespace.Rect({
2984
- originX: "left",
2985
- originY: "top",
2986
- left: 0,
2987
- top: 0,
2988
- width: frameWidth,
2989
- height: frameHeight,
2990
- fill: "transparent",
2991
- stroke: "transparent",
2992
- strokeWidth: 0,
2993
- rx: element.clipShape === "rounded" ? element.rx || 8 : 0,
2994
- ry: element.clipShape === "rounded" ? element.ry || 8 : 0
2995
- });
2996
- const group = new fabric__namespace.Group([bgRect], {
2997
- left: element.left,
2998
- top: element.top,
3077
+ left: (element.left ?? 0) + visualWidth / 2,
3078
+ top: (element.top ?? 0) + visualHeight / 2,
2999
3079
  originX: "center",
3000
3080
  originY: "center",
3001
- width: frameWidth,
3002
- height: frameHeight,
3081
+ width: visualWidth,
3082
+ height: visualHeight,
3003
3083
  scaleX: 1,
3004
3084
  scaleY: 1,
3005
3085
  angle: element.angle ?? 0,
3006
3086
  opacity: element.opacity ?? 1,
3007
3087
  flipX: element.flipX ?? false,
3008
- flipY: element.flipY ?? false
3088
+ flipY: element.flipY ?? false,
3089
+ selectable: element.selectable,
3090
+ evented: element.evented,
3091
+ hasControls: true,
3092
+ hasBorders: true,
3093
+ interactive: true,
3094
+ subTargetCheck: false,
3095
+ objectCaching: false
3009
3096
  });
3010
- if (element.clipShape && element.clipShape !== "none") {
3011
- const clipPath = createImageClipPath(element, frameWidth, frameHeight);
3012
- if (clipPath) {
3013
- clipPath.absolutePositioned = false;
3014
- clipPath.excludeFromExport = true;
3015
- group.clipPath = clipPath;
3016
- }
3097
+ group.__emptyImagePlaceholder = true;
3098
+ group.__imageSrc = "";
3099
+ group.__cropData = {
3100
+ shape: element.clipShape === "circle" ? "circle" : element.clipShape === "rounded" ? "roundRect" : "rect",
3101
+ rx: element.clipShape === "rounded" ? element.rx || 0.1 : 0,
3102
+ frameW: visualWidth,
3103
+ frameH: visualHeight,
3104
+ fit: "cover",
3105
+ _img: null,
3106
+ _border: null,
3107
+ _placeholderFrame: bgRect
3108
+ };
3109
+ group._ct = group._ct || {};
3110
+ group._ct.isCropGroup = true;
3111
+ group.__cropGroup = true;
3112
+ const clipPath = createImageClipPath(
3113
+ {
3114
+ ...element,
3115
+ clipShape: element.clipShape && element.clipShape !== "none" ? element.clipShape : "rectangle"
3116
+ },
3117
+ visualWidth,
3118
+ visualHeight
3119
+ );
3120
+ if (clipPath) {
3121
+ clipPath.absolutePositioned = false;
3122
+ clipPath.excludeFromExport = true;
3123
+ group.clipPath = clipPath;
3124
+ }
3125
+ stabilizePlaceholderGroup(group, visualWidth, visualHeight);
3126
+ if (!hasImage) {
3127
+ attachEmptyPlaceholderImage(group, visualWidth, visualHeight);
3017
3128
  }
3018
3129
  return group;
3019
3130
  }
3131
+ function createImagePlaceholderForGroup(element) {
3132
+ return createImagePlaceholder(element);
3133
+ }
3020
3134
  function createImageClipPath(element, imgWidth, imgHeight) {
3021
3135
  const clipShape = element.clipShape || "none";
3022
3136
  if (clipShape === "none") return void 0;
@@ -3054,7 +3168,328 @@ function createImageClipPath(element, imgWidth, imgHeight) {
3054
3168
  return void 0;
3055
3169
  }
3056
3170
  }
3057
- function clamp(v, min, max) {
3171
+ const isCropGroup = (obj) => {
3172
+ var _a;
3173
+ const g = obj;
3174
+ return Boolean(g && (g.__cropGroup || ((_a = g._ct) == null ? void 0 : _a.isCropGroup)));
3175
+ };
3176
+ function parseLuminance(fill) {
3177
+ if (typeof fill !== "string") return null;
3178
+ const f = fill.trim().toLowerCase();
3179
+ if (!f || f === "none" || f === "transparent") return null;
3180
+ if (f === "white" || f === "#fff" || f === "#ffffff") return 1;
3181
+ if (f === "black" || f === "#000" || f === "#000000") return 0;
3182
+ const hex = f.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
3183
+ if (hex) {
3184
+ const h = hex[1];
3185
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
3186
+ const r = parseInt(full.slice(0, 2), 16) / 255;
3187
+ const g = parseInt(full.slice(2, 4), 16) / 255;
3188
+ const b = parseInt(full.slice(4, 6), 16) / 255;
3189
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
3190
+ }
3191
+ const rgb = f.match(/^rgba?\(([^)]+)\)$/);
3192
+ if (rgb) {
3193
+ const parts = rgb[1].split(",").map((s) => parseFloat(s.trim()));
3194
+ if (parts.length >= 3) {
3195
+ const r = parts[0] / 255;
3196
+ const g = parts[1] / 255;
3197
+ const b = parts[2] / 255;
3198
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
3199
+ }
3200
+ }
3201
+ return null;
3202
+ }
3203
+ function getObjectRenderedSize(obj) {
3204
+ const bbox = typeof obj.getBoundingRect === "function" ? obj.getBoundingRect() : null;
3205
+ const width = Math.abs(Number(bbox == null ? void 0 : bbox.width) || (obj.width ?? 0) * Math.abs(obj.scaleX ?? 1));
3206
+ const height = Math.abs(Number(bbox == null ? void 0 : bbox.height) || (obj.height ?? 0) * Math.abs(obj.scaleY ?? 1));
3207
+ return { width, height };
3208
+ }
3209
+ function isLikelyFullFrameBackground(obj, luminance, viewBoxW, viewBoxH, totalObjects) {
3210
+ if (totalObjects <= 1 || viewBoxW <= 0 || viewBoxH <= 0 || luminance === null) return false;
3211
+ const { width, height } = getObjectRenderedSize(obj);
3212
+ const widthRatio = width / viewBoxW;
3213
+ const heightRatio = height / viewBoxH;
3214
+ const areaRatio = width * height / (viewBoxW * viewBoxH);
3215
+ return widthRatio >= 0.96 && heightRatio >= 0.96 && areaRatio >= 0.9;
3216
+ }
3217
+ async function loadSvgAsGroup(url) {
3218
+ const result = await fabric__namespace.loadSVGFromURL(url);
3219
+ const allObjects = (result.objects || []).filter(Boolean);
3220
+ const options = result.options || {};
3221
+ if (allObjects.length === 0) {
3222
+ throw new Error("SVG contained no parseable shapes");
3223
+ }
3224
+ const hintedViewBoxW = Number(options.width) || 0;
3225
+ const hintedViewBoxH = Number(options.height) || 0;
3226
+ const sourceLuminances = allObjects.map((o) => parseLuminance(o.fill));
3227
+ let objects = allObjects.filter((obj, i) => !isLikelyFullFrameBackground(
3228
+ obj,
3229
+ sourceLuminances[i],
3230
+ hintedViewBoxW,
3231
+ hintedViewBoxH,
3232
+ allObjects.length
3233
+ ));
3234
+ if (objects.length === 0) objects = allObjects;
3235
+ if (objects.length === 0) objects = allObjects;
3236
+ for (const obj of objects) {
3237
+ obj.fillRule = "evenodd";
3238
+ obj.fill = "#000";
3239
+ obj.stroke = null;
3240
+ obj.strokeWidth = 0;
3241
+ }
3242
+ const group = new fabric__namespace.Group(objects, {
3243
+ originX: "center",
3244
+ originY: "center",
3245
+ selectable: false,
3246
+ evented: false,
3247
+ hasControls: false,
3248
+ hasBorders: false
3249
+ });
3250
+ const viewBoxW = Number(options.width) || group.width || 1;
3251
+ const viewBoxH = Number(options.height) || group.height || 1;
3252
+ return { group, viewBoxW, viewBoxH };
3253
+ }
3254
+ function fitMaskGroupToFrame(maskGroup, frameW, frameH) {
3255
+ const viewBoxW = Number(maskGroup.__svgMaskViewBoxW) || maskGroup.width || 1;
3256
+ const viewBoxH = Number(maskGroup.__svgMaskViewBoxH) || maskGroup.height || 1;
3257
+ maskGroup.set({
3258
+ left: 0,
3259
+ top: 0,
3260
+ originX: "center",
3261
+ originY: "center",
3262
+ scaleX: frameW / viewBoxW,
3263
+ scaleY: frameH / viewBoxH,
3264
+ selectable: false,
3265
+ evented: false,
3266
+ hasControls: false,
3267
+ hasBorders: false
3268
+ });
3269
+ maskGroup.absolutePositioned = false;
3270
+ maskGroup.excludeFromExport = true;
3271
+ maskGroup.inverted = false;
3272
+ maskGroup.dirty = true;
3273
+ maskGroup.setCoords();
3274
+ }
3275
+ function isSvgMaskClipPath(clipPath) {
3276
+ return Boolean(clipPath && clipPath.__svgMask && clipPath instanceof fabric__namespace.Group);
3277
+ }
3278
+ function isLuminanceMaskClipPath(clipPath) {
3279
+ return Boolean(clipPath && clipPath.__svgMaskType === "luminance");
3280
+ }
3281
+ function syncSvgMaskClipPath(cropGroup) {
3282
+ var _a, _b;
3283
+ const clipPath = cropGroup.clipPath;
3284
+ if (!isCropGroup(cropGroup)) return;
3285
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3286
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3287
+ if (frameW <= 0 || frameH <= 0) return;
3288
+ if (isSvgMaskClipPath(clipPath)) {
3289
+ fitMaskGroupToFrame(clipPath, frameW, frameH);
3290
+ }
3291
+ }
3292
+ async function detectMaskType(svgUrl) {
3293
+ try {
3294
+ const res = await fetch(svgUrl);
3295
+ if (!res.ok) return "shape";
3296
+ const text = await res.text();
3297
+ const t = text.toLowerCase();
3298
+ if (t.includes("<lineargradient") || t.includes("<radialgradient")) return "luminance";
3299
+ if (t.includes("<image ") || t.includes("<image>")) return "luminance";
3300
+ if (t.includes('mask-type="luminance"') || t.includes("mask-type='luminance'")) return "luminance";
3301
+ if (t.includes("<filter")) return "luminance";
3302
+ if (/fill-opacity\s*=\s*["'](0?\.\d+)/.test(t)) return "luminance";
3303
+ if (/opacity\s*=\s*["'](0?\.\d+)/.test(t)) return "luminance";
3304
+ return "shape";
3305
+ } catch {
3306
+ return "shape";
3307
+ }
3308
+ }
3309
+ async function applySvgMaskToCropGroup(cropGroup, svgUrl) {
3310
+ var _a, _b, _c;
3311
+ if (!isCropGroup(cropGroup)) {
3312
+ throw new Error("Selected object is not a crop group / image");
3313
+ }
3314
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3315
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3316
+ if (frameW <= 0 || frameH <= 0) {
3317
+ throw new Error("Crop group has no frame dimensions");
3318
+ }
3319
+ const { group: maskGroup, viewBoxW, viewBoxH } = await loadSvgAsGroup(svgUrl);
3320
+ const mw = maskGroup.width ?? 0;
3321
+ const mh = maskGroup.height ?? 0;
3322
+ if (mw <= 0 || mh <= 0 || viewBoxW <= 0 || viewBoxH <= 0) {
3323
+ throw new Error("This mask has no usable shapes (try the 'Soft' mode instead)");
3324
+ }
3325
+ maskGroup.__svgMask = true;
3326
+ maskGroup.__svgMaskUrl = svgUrl;
3327
+ maskGroup.__svgMaskType = "shape";
3328
+ maskGroup.__svgMaskViewBoxW = viewBoxW;
3329
+ maskGroup.__svgMaskViewBoxH = viewBoxH;
3330
+ fitMaskGroupToFrame(maskGroup, frameW, frameH);
3331
+ cropGroup.clipPath = maskGroup;
3332
+ cropGroup.__svgMaskUrl = svgUrl;
3333
+ cropGroup.__svgMaskType = "shape";
3334
+ cropGroup.dirty = true;
3335
+ if (cropGroup._objects) {
3336
+ for (const child of cropGroup._objects) {
3337
+ child.dirty = true;
3338
+ }
3339
+ }
3340
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3341
+ }
3342
+ function loadSvgAsImage(svgUrl) {
3343
+ return new Promise((resolve, reject) => {
3344
+ const tryFetch = async () => {
3345
+ try {
3346
+ const res = await fetch(svgUrl, { mode: "cors" });
3347
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3348
+ const blob = await res.blob();
3349
+ const objectUrl = URL.createObjectURL(blob);
3350
+ const img2 = new Image();
3351
+ img2.onload = () => resolve(img2);
3352
+ img2.onerror = () => reject(new Error("Failed to load SVG (after fetch fallback)"));
3353
+ img2.src = objectUrl;
3354
+ } catch (e) {
3355
+ reject(new Error(`Failed to load SVG: ${(e == null ? void 0 : e.message) || "network error"}`));
3356
+ }
3357
+ };
3358
+ const img = new Image();
3359
+ img.crossOrigin = "anonymous";
3360
+ img.onload = () => resolve(img);
3361
+ img.onerror = () => tryFetch();
3362
+ img.src = svgUrl;
3363
+ });
3364
+ }
3365
+ async function buildLuminanceAlphaCanvas(svgUrl, frameW, frameH) {
3366
+ const w = Math.max(2, Math.round(frameW));
3367
+ const h = Math.max(2, Math.round(frameH));
3368
+ const img = await loadSvgAsImage(svgUrl);
3369
+ const canvas = document.createElement("canvas");
3370
+ canvas.width = w;
3371
+ canvas.height = h;
3372
+ const ctx = canvas.getContext("2d");
3373
+ if (!ctx) throw new Error("Failed to get 2D context for mask canvas");
3374
+ ctx.clearRect(0, 0, w, h);
3375
+ ctx.drawImage(img, 0, 0, w, h);
3376
+ let data;
3377
+ try {
3378
+ data = ctx.getImageData(0, 0, w, h);
3379
+ } catch (e) {
3380
+ throw new Error(
3381
+ "Could not read SVG pixels (CORS / tainted canvas). Try a different mask source."
3382
+ );
3383
+ }
3384
+ const px = data.data;
3385
+ for (let i = 0; i < px.length; i += 4) {
3386
+ const r = px[i];
3387
+ const g = px[i + 1];
3388
+ const b = px[i + 2];
3389
+ const a = px[i + 3];
3390
+ const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
3391
+ const alpha = lum / 255 * a;
3392
+ px[i] = 255;
3393
+ px[i + 1] = 255;
3394
+ px[i + 2] = 255;
3395
+ px[i + 3] = Math.round(alpha);
3396
+ }
3397
+ ctx.putImageData(data, 0, 0);
3398
+ return canvas;
3399
+ }
3400
+ async function applyLuminanceMaskToCropGroup(cropGroup, svgUrl) {
3401
+ var _a, _b, _c;
3402
+ if (!isCropGroup(cropGroup)) {
3403
+ throw new Error("Selected object is not a crop group / image");
3404
+ }
3405
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3406
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3407
+ if (frameW <= 0 || frameH <= 0) {
3408
+ throw new Error("Crop group has no frame dimensions");
3409
+ }
3410
+ const alphaCanvas = await buildLuminanceAlphaCanvas(svgUrl, frameW, frameH);
3411
+ const maskImg = new fabric__namespace.FabricImage(alphaCanvas, {
3412
+ originX: "center",
3413
+ originY: "center",
3414
+ left: 0,
3415
+ top: 0,
3416
+ selectable: false,
3417
+ evented: false,
3418
+ hasControls: false,
3419
+ hasBorders: false
3420
+ });
3421
+ maskImg.absolutePositioned = false;
3422
+ maskImg.excludeFromExport = true;
3423
+ maskImg.inverted = false;
3424
+ maskImg.__svgMask = true;
3425
+ maskImg.__svgMaskUrl = svgUrl;
3426
+ maskImg.__svgMaskType = "luminance";
3427
+ cropGroup.clipPath = maskImg;
3428
+ cropGroup.__svgMaskUrl = svgUrl;
3429
+ cropGroup.__svgMaskType = "luminance";
3430
+ cropGroup.dirty = true;
3431
+ if (cropGroup._objects) {
3432
+ for (const child of cropGroup._objects) {
3433
+ child.dirty = true;
3434
+ }
3435
+ }
3436
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3437
+ }
3438
+ async function applyMaskToCropGroup(cropGroup, svgUrl, maskType) {
3439
+ if (maskType === "luminance") {
3440
+ return applyLuminanceMaskToCropGroup(cropGroup, svgUrl);
3441
+ }
3442
+ return applySvgMaskToCropGroup(cropGroup, svgUrl);
3443
+ }
3444
+ function getAppliedSvgMaskUrl(obj) {
3445
+ if (!obj) return null;
3446
+ const url = obj.__svgMaskUrl;
3447
+ return typeof url === "string" ? url : null;
3448
+ }
3449
+ function getAppliedSvgMaskType(obj) {
3450
+ if (!obj) return null;
3451
+ const t = obj.__svgMaskType;
3452
+ return t === "luminance" || t === "shape" ? t : null;
3453
+ }
3454
+ function clearSvgMaskFromCropGroup(cropGroup) {
3455
+ var _a, _b, _c;
3456
+ if (!isCropGroup(cropGroup)) return;
3457
+ const frameW = ((_a = cropGroup._ct) == null ? void 0 : _a.frameW) ?? cropGroup.width ?? 0;
3458
+ const frameH = ((_b = cropGroup._ct) == null ? void 0 : _b.frameH) ?? cropGroup.height ?? 0;
3459
+ const rect = new fabric__namespace.Rect({
3460
+ width: frameW,
3461
+ height: frameH,
3462
+ left: 0,
3463
+ top: 0,
3464
+ originX: "center",
3465
+ originY: "center",
3466
+ selectable: false,
3467
+ evented: false,
3468
+ hasControls: false,
3469
+ hasBorders: false
3470
+ });
3471
+ rect.absolutePositioned = false;
3472
+ rect.excludeFromExport = true;
3473
+ cropGroup.clipPath = rect;
3474
+ delete cropGroup.__svgMaskUrl;
3475
+ delete cropGroup.__svgMaskType;
3476
+ cropGroup.dirty = true;
3477
+ (_c = cropGroup.canvas) == null ? void 0 : _c.requestRenderAll();
3478
+ }
3479
+ const svgMaskApply = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
3480
+ __proto__: null,
3481
+ applyLuminanceMaskToCropGroup,
3482
+ applyMaskToCropGroup,
3483
+ applySvgMaskToCropGroup,
3484
+ clearSvgMaskFromCropGroup,
3485
+ detectMaskType,
3486
+ getAppliedSvgMaskType,
3487
+ getAppliedSvgMaskUrl,
3488
+ isLuminanceMaskClipPath,
3489
+ isSvgMaskClipPath,
3490
+ syncSvgMaskClipPath
3491
+ }, Symbol.toStringTag, { value: "Module" }));
3492
+ function clamp$1(v, min, max) {
3058
3493
  return Math.max(min, Math.min(max, v));
3059
3494
  }
3060
3495
  function applyControlSizeForZoom(canvas, obj) {
@@ -3078,76 +3513,78 @@ function finalizeCropGroupCoords(g) {
3078
3513
  g.setCoords();
3079
3514
  }
3080
3515
  function updateCoverLayout(g) {
3516
+ var _a;
3081
3517
  const ct = g.__cropData;
3082
3518
  if (!ct) return;
3083
3519
  const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
3084
- if (!img) return;
3085
3520
  const minDim = Math.min(frameW, frameH);
3086
3521
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
3087
3522
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
3088
- const needsNewClipPath = !g.clipPath || shape === "circle" && !(g.clipPath instanceof fabric__namespace.Ellipse) || shape !== "circle" && !(g.clipPath instanceof fabric__namespace.Rect);
3089
- if (needsNewClipPath) {
3090
- const clip = shape === "circle" ? new fabric__namespace.Ellipse({
3091
- rx: frameW / 2,
3092
- ry: frameH / 2,
3093
- left: 0,
3094
- top: 0,
3095
- originX: "center",
3096
- originY: "center",
3097
- selectable: false,
3098
- evented: false,
3099
- hasControls: false,
3100
- hasBorders: false
3101
- }) : new fabric__namespace.Rect({
3102
- width: frameW,
3103
- height: frameH,
3104
- rx,
3105
- // rx is already calculated from ratio above
3106
- ry: rx,
3107
- // Ensure both are the same for uniform corners
3108
- left: 0,
3109
- top: 0,
3110
- originX: "center",
3111
- originY: "center",
3112
- selectable: false,
3113
- evented: false,
3114
- hasControls: false,
3115
- hasBorders: false
3116
- });
3117
- clip.absolutePositioned = false;
3118
- clip.excludeFromExport = true;
3119
- clip.setCoords();
3120
- clip.dirty = true;
3121
- g.clipPath = clip;
3122
- } else if (g.clipPath && typeof g.clipPath.set === "function") {
3123
- if (shape === "circle") {
3124
- g.clipPath.set({
3523
+ const currentClipPath = g.clipPath;
3524
+ const hasCustomSvgMask = Boolean(
3525
+ currentClipPath && (isSvgMaskClipPath(currentClipPath) || isLuminanceMaskClipPath(currentClipPath) || currentClipPath.__svgMask)
3526
+ );
3527
+ if (hasCustomSvgMask) {
3528
+ if (isSvgMaskClipPath(currentClipPath)) {
3529
+ syncSvgMaskClipPath(g);
3530
+ } else if (currentClipPath && typeof currentClipPath.set === "function") {
3531
+ const baseW = Math.max(1, Number(currentClipPath.width) || frameW);
3532
+ const baseH = Math.max(1, Number(currentClipPath.height) || frameH);
3533
+ currentClipPath.set({
3534
+ left: 0,
3535
+ top: 0,
3536
+ originX: "center",
3537
+ originY: "center",
3538
+ scaleX: frameW / baseW,
3539
+ scaleY: frameH / baseH,
3540
+ selectable: false,
3541
+ evented: false,
3542
+ hasControls: false,
3543
+ hasBorders: false
3544
+ });
3545
+ currentClipPath.absolutePositioned = false;
3546
+ currentClipPath.excludeFromExport = true;
3547
+ currentClipPath.dirty = true;
3548
+ currentClipPath.setCoords();
3549
+ }
3550
+ } else {
3551
+ const needsNewClipPath = !g.clipPath || shape === "circle" && !(g.clipPath instanceof fabric__namespace.Ellipse) || shape !== "circle" && !(g.clipPath instanceof fabric__namespace.Rect);
3552
+ if (needsNewClipPath) {
3553
+ const clip = shape === "circle" ? new fabric__namespace.Ellipse({
3125
3554
  rx: frameW / 2,
3126
3555
  ry: frameH / 2,
3127
3556
  left: 0,
3128
3557
  top: 0,
3129
3558
  originX: "center",
3130
3559
  originY: "center",
3131
- // CRITICAL: Keep clipPath non-interactive
3560
+ selectable: false,
3561
+ evented: false,
3562
+ hasControls: false,
3563
+ hasBorders: false
3564
+ }) : new fabric__namespace.Rect({
3565
+ width: frameW,
3566
+ height: frameH,
3567
+ rx,
3568
+ ry: rx,
3569
+ left: 0,
3570
+ top: 0,
3571
+ originX: "center",
3572
+ originY: "center",
3132
3573
  selectable: false,
3133
3574
  evented: false,
3134
3575
  hasControls: false,
3135
3576
  hasBorders: false
3136
3577
  });
3137
- } else {
3138
- const clipPathObj = g.clipPath;
3139
- const currentRx = clipPathObj.rx || 0;
3140
- const currentWidth = clipPathObj.width || 0;
3141
- const currentHeight = clipPathObj.height || 0;
3142
- const rxChanged = Math.abs(currentRx - rx) > 0.01;
3143
- const dimsChanged = Math.abs(currentWidth - frameW) > 0.1 || Math.abs(currentHeight - frameH) > 0.1;
3144
- if (rxChanged || dimsChanged) {
3145
- const newClip = new fabric__namespace.Rect({
3146
- width: frameW,
3147
- height: frameH,
3148
- rx,
3149
- ry: rx,
3150
- // Ensure both are identical for uniform corners
3578
+ clip.absolutePositioned = false;
3579
+ clip.excludeFromExport = true;
3580
+ clip.setCoords();
3581
+ clip.dirty = true;
3582
+ g.clipPath = clip;
3583
+ } else if (g.clipPath && typeof g.clipPath.set === "function") {
3584
+ if (shape === "circle") {
3585
+ g.clipPath.set({
3586
+ rx: frameW / 2,
3587
+ ry: frameH / 2,
3151
3588
  left: 0,
3152
3589
  top: 0,
3153
3590
  originX: "center",
@@ -3157,32 +3594,55 @@ function updateCoverLayout(g) {
3157
3594
  hasControls: false,
3158
3595
  hasBorders: false
3159
3596
  });
3160
- newClip.absolutePositioned = false;
3161
- newClip.excludeFromExport = true;
3162
- newClip.setCoords();
3163
- newClip.dirty = true;
3164
- g.clipPath = newClip;
3165
3597
  } else {
3166
- clipPathObj.set({
3167
- width: frameW,
3168
- height: frameH,
3169
- rx,
3170
- ry: rx,
3171
- left: 0,
3172
- top: 0,
3173
- originX: "center",
3174
- originY: "center",
3175
- selectable: false,
3176
- evented: false,
3177
- hasControls: false,
3178
- hasBorders: false
3179
- });
3180
- clipPathObj.setCoords();
3181
- clipPathObj.dirty = true;
3598
+ const clipPathObj = g.clipPath;
3599
+ const currentRx = clipPathObj.rx || 0;
3600
+ const currentWidth = clipPathObj.width || 0;
3601
+ const currentHeight = clipPathObj.height || 0;
3602
+ const rxChanged = Math.abs(currentRx - rx) > 0.01;
3603
+ const dimsChanged = Math.abs(currentWidth - frameW) > 0.1 || Math.abs(currentHeight - frameH) > 0.1;
3604
+ if (rxChanged || dimsChanged) {
3605
+ const newClip = new fabric__namespace.Rect({
3606
+ width: frameW,
3607
+ height: frameH,
3608
+ rx,
3609
+ ry: rx,
3610
+ left: 0,
3611
+ top: 0,
3612
+ originX: "center",
3613
+ originY: "center",
3614
+ selectable: false,
3615
+ evented: false,
3616
+ hasControls: false,
3617
+ hasBorders: false
3618
+ });
3619
+ newClip.absolutePositioned = false;
3620
+ newClip.excludeFromExport = true;
3621
+ newClip.setCoords();
3622
+ newClip.dirty = true;
3623
+ g.clipPath = newClip;
3624
+ } else {
3625
+ clipPathObj.set({
3626
+ width: frameW,
3627
+ height: frameH,
3628
+ rx,
3629
+ ry: rx,
3630
+ left: 0,
3631
+ top: 0,
3632
+ originX: "center",
3633
+ originY: "center",
3634
+ selectable: false,
3635
+ evented: false,
3636
+ hasControls: false,
3637
+ hasBorders: false
3638
+ });
3639
+ clipPathObj.setCoords();
3640
+ clipPathObj.dirty = true;
3641
+ }
3182
3642
  }
3643
+ g.clipPath.absolutePositioned = false;
3644
+ g.clipPath.excludeFromExport = true;
3183
3645
  }
3184
- g.clipPath.absolutePositioned = false;
3185
- g.clipPath.excludeFromExport = true;
3186
3646
  }
3187
3647
  if (border) {
3188
3648
  if (shape === "circle") {
@@ -3194,6 +3654,28 @@ function updateCoverLayout(g) {
3194
3654
  border.set({ width: frameW, height: frameH, rx: actualRx, ry: actualRx });
3195
3655
  }
3196
3656
  }
3657
+ if (!img) {
3658
+ const placeholderFrame = ct._placeholderFrame ?? ((_a = g._objects) == null ? void 0 : _a.find((obj) => obj.__isPlaceholderFrame));
3659
+ if (placeholderFrame && typeof placeholderFrame.set === "function") {
3660
+ placeholderFrame.set({
3661
+ width: frameW,
3662
+ height: frameH,
3663
+ left: 0,
3664
+ top: 0,
3665
+ originX: "center",
3666
+ originY: "center",
3667
+ ...placeholderFrame instanceof fabric__namespace.Rect ? { rx, ry: rx } : {}
3668
+ });
3669
+ placeholderFrame.dirty = true;
3670
+ ct._placeholderFrame = placeholderFrame;
3671
+ }
3672
+ finalizeCropGroupCoords(g);
3673
+ if (g.canvas) {
3674
+ g.setCoords();
3675
+ g.canvas.requestRenderAll();
3676
+ }
3677
+ return;
3678
+ }
3197
3679
  img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
3198
3680
  if (img.__panX !== void 0 || img.__panY !== void 0) {
3199
3681
  img._ct.panX = img.__panX ?? 0.5;
@@ -3215,8 +3697,8 @@ function updateCoverLayout(g) {
3215
3697
  const dispH = ih * finalScale;
3216
3698
  const overflowX = Math.max(0, dispW - frameW);
3217
3699
  const overflowY = Math.max(0, dispH - frameH);
3218
- const panX = clamp(img._ct.panX ?? 0.5, 0, 1);
3219
- const panY = clamp(img._ct.panY ?? 0.5, 0, 1);
3700
+ const panX = clamp$1(img._ct.panX ?? 0.5, 0, 1);
3701
+ const panY = clamp$1(img._ct.panY ?? 0.5, 0, 1);
3220
3702
  const offsetX = fitContain ? 0 : overflowX > 0 ? -overflowX * (panX - 0.5) : 0;
3221
3703
  const offsetY = fitContain ? 0 : overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
3222
3704
  img.set({ left: offsetX, top: offsetY });
@@ -3308,51 +3790,6 @@ function getRotatedControlCursor(controlKey, target) {
3308
3790
  const makeRotatedCursorStyleHandler = (controlKey) => {
3309
3791
  return (_eventData, _control, target) => getRotatedControlCursor(controlKey, target);
3310
3792
  };
3311
- function resizeFrameFromCorner(eventData, transform, _x, _y) {
3312
- var _a;
3313
- const g = transform.target;
3314
- const ct = g.__cropData;
3315
- if (!ct || !((_a = g._ct) == null ? void 0 : _a.isCropGroup)) return false;
3316
- const canvas = g.canvas;
3317
- if (!canvas) return false;
3318
- const e = getDomEvent(eventData);
3319
- if (!e) return false;
3320
- const pointer = canvas.getPointer(e);
3321
- g.setCoords();
3322
- const a = g.aCoords;
3323
- if (!a) return false;
3324
- const corner = transform.corner;
3325
- const anchor = getOppositeAnchor(a, corner);
3326
- const MIN_W = 20;
3327
- const MIN_H = 20;
3328
- const angle = g.angle || 0;
3329
- const deltaWorldX = pointer.x - anchor.x;
3330
- const deltaWorldY = pointer.y - anchor.y;
3331
- const localDelta = worldDeltaToLocal(deltaWorldX, deltaWorldY, angle);
3332
- const defaultSigns = getCornerDefaultSigns(corner);
3333
- const signX = Math.abs(localDelta.x) < 1e-3 ? defaultSigns.x : localDelta.x >= 0 ? 1 : -1;
3334
- const signY = Math.abs(localDelta.y) < 1e-3 ? defaultSigns.y : localDelta.y >= 0 ? 1 : -1;
3335
- const newW = Math.max(MIN_W, Math.abs(localDelta.x));
3336
- const newH = Math.max(MIN_H, Math.abs(localDelta.y));
3337
- ct.frameW = newW;
3338
- ct.frameH = newH;
3339
- const centerLocal = {
3340
- x: signX * (newW / 2),
3341
- y: signY * (newH / 2)
3342
- };
3343
- const centerWorld = localDeltaToWorld(centerLocal.x, centerLocal.y, angle);
3344
- g.set({
3345
- left: anchor.x + centerWorld.x,
3346
- top: anchor.y + centerWorld.y,
3347
- originX: "center",
3348
- originY: "center",
3349
- width: newW,
3350
- height: newH
3351
- });
3352
- updateCoverLayout(g);
3353
- canvas.requestRenderAll();
3354
- return true;
3355
- }
3356
3793
  function resizeFrameFromCornerUniform(eventData, transform, _x, _y) {
3357
3794
  var _a;
3358
3795
  const g = transform.target;
@@ -3432,7 +3869,7 @@ function resizeFrameFromSide(g, side, localDx, localDy) {
3432
3869
  }
3433
3870
  function installCanvaMaskControls(g) {
3434
3871
  const ct = g.__cropData;
3435
- if (ct) ct.fit = void 0;
3872
+ if (ct) ct.fit = ct.fit ?? "cover";
3436
3873
  g.setControlsVisibility({
3437
3874
  mt: true,
3438
3875
  mb: true,
@@ -3486,7 +3923,7 @@ function installCanvaMaskControls(g) {
3486
3923
  if (canvas && canvas.__editLockRef) {
3487
3924
  canvas.__editLockRef.current = true;
3488
3925
  }
3489
- return resizeFrameFromCorner(eventData, transform);
3926
+ return resizeFrameFromCornerUniform(eventData, transform);
3490
3927
  }
3491
3928
  });
3492
3929
  };
@@ -3496,47 +3933,6 @@ function installCanvaMaskControls(g) {
3496
3933
  g.controls.br = makeCornerControl("br", 0.5, 0.5, "nwse-resize");
3497
3934
  g.__hasCustomControls = true;
3498
3935
  }
3499
- function setSimpleScaleControls(g) {
3500
- if (!g.__cropGroup) return;
3501
- const ct = g.__cropData;
3502
- if (ct) {
3503
- updateCoverLayout(g);
3504
- }
3505
- g.lockScalingX = true;
3506
- g.lockScalingY = true;
3507
- g.setControlsVisibility({
3508
- mt: false,
3509
- mb: false,
3510
- ml: false,
3511
- mr: false,
3512
- tl: true,
3513
- tr: true,
3514
- bl: true,
3515
- br: true,
3516
- mtr: true
3517
- });
3518
- g.padding = 0;
3519
- const makeCornerControl = (key, x, y, cursor) => new fabric__namespace.Control({
3520
- x,
3521
- y,
3522
- cursorStyle: cursor,
3523
- cursorStyleHandler: makeRotatedCursorStyleHandler(key),
3524
- actionName: "resize",
3525
- actionHandler: (eventData, transform) => {
3526
- const t = transform.target;
3527
- if (t.canvas && t.canvas.__editLockRef) {
3528
- t.canvas.__editLockRef.current = true;
3529
- }
3530
- return resizeFrameFromCornerUniform(eventData, transform);
3531
- }
3532
- });
3533
- g.controls.tl = makeCornerControl("tl", -0.5, -0.5, "nwse-resize");
3534
- g.controls.tr = makeCornerControl("tr", 0.5, -0.5, "nesw-resize");
3535
- g.controls.bl = makeCornerControl("bl", -0.5, 0.5, "nesw-resize");
3536
- g.controls.br = makeCornerControl("br", 0.5, 0.5, "nwse-resize");
3537
- g.__hasCustomControls = true;
3538
- g.setCoords();
3539
- }
3540
3936
  async function createMaskedImageElement({
3541
3937
  url,
3542
3938
  image,
@@ -3719,9 +4115,405 @@ async function createMaskedImageElement({
3719
4115
  });
3720
4116
  g.setCoords();
3721
4117
  updateCoverLayout(g);
3722
- setSimpleScaleControls(g);
4118
+ installCanvaMaskControls(g);
3723
4119
  return g;
3724
4120
  }
4121
+ const CROP_MODE_FLAG = "__inCropMode";
4122
+ const CROP_GHOST_FLAG = "__cropGhost";
4123
+ const CROP_OUTLINE_FLAG = "__cropOutline";
4124
+ const CROP_HANDLERS_FLAG = "__cropHandlers";
4125
+ function isCropGroupInCropMode(g) {
4126
+ return Boolean(g[CROP_MODE_FLAG]);
4127
+ }
4128
+ function clamp(v, min, max) {
4129
+ return Math.max(min, Math.min(max, v));
4130
+ }
4131
+ function syncGhostTransform(g) {
4132
+ const ghost = g[CROP_GHOST_FLAG];
4133
+ const ct = g.__cropData;
4134
+ if (!ghost || !(ct == null ? void 0 : ct._img)) return;
4135
+ const img = ct._img;
4136
+ const finalScale = img.scaleX || 1;
4137
+ const gLeft = g.left ?? 0;
4138
+ const gTop = g.top ?? 0;
4139
+ const angle = g.angle ?? 0;
4140
+ const imgLocalLeft = img.left ?? 0;
4141
+ const imgLocalTop = img.top ?? 0;
4142
+ const rad = fabric__namespace.util.degreesToRadians(angle);
4143
+ const cos = Math.cos(rad);
4144
+ const sin = Math.sin(rad);
4145
+ const worldDx = imgLocalLeft * cos - imgLocalTop * sin;
4146
+ const worldDy = imgLocalLeft * sin + imgLocalTop * cos;
4147
+ ghost.set({
4148
+ left: gLeft + worldDx,
4149
+ top: gTop + worldDy,
4150
+ scaleX: finalScale,
4151
+ scaleY: finalScale,
4152
+ angle,
4153
+ originX: "center",
4154
+ originY: "center"
4155
+ });
4156
+ ghost.setCoords();
4157
+ }
4158
+ function syncOutlineTransform(g) {
4159
+ const outline = g[CROP_OUTLINE_FLAG];
4160
+ const ct = g.__cropData;
4161
+ if (!outline || !ct) return;
4162
+ outline.set({
4163
+ left: g.left ?? 0,
4164
+ top: g.top ?? 0,
4165
+ width: ct.frameW,
4166
+ height: ct.frameH,
4167
+ angle: g.angle ?? 0,
4168
+ originX: "center",
4169
+ originY: "center"
4170
+ });
4171
+ if (outline instanceof fabric__namespace.Rect) {
4172
+ const minDim = Math.min(ct.frameW, ct.frameH);
4173
+ const rxRatio = ct.rx ?? 0;
4174
+ let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
4175
+ rx = Math.max(0, Math.min(rx, ct.frameW / 2, ct.frameH / 2));
4176
+ outline.set({ rx, ry: rx });
4177
+ }
4178
+ outline.setCoords();
4179
+ }
4180
+ function installCropModeVisuals(g) {
4181
+ g.setControlsVisibility({
4182
+ mt: false,
4183
+ mb: false,
4184
+ ml: false,
4185
+ mr: false,
4186
+ tl: false,
4187
+ tr: false,
4188
+ bl: false,
4189
+ br: false,
4190
+ mtr: false
4191
+ });
4192
+ g.hasControls = false;
4193
+ g.lockScalingX = true;
4194
+ g.lockScalingY = true;
4195
+ g.lockRotation = true;
4196
+ g.lockMovementX = false;
4197
+ g.lockMovementY = false;
4198
+ g.borderColor = "hsl(256, 80%, 58%)";
4199
+ g.borderDashArray = [4, 4];
4200
+ g.setCoords();
4201
+ }
4202
+ function enterCropMode(g) {
4203
+ const ct = g.__cropData;
4204
+ if (!(ct == null ? void 0 : ct._img)) return false;
4205
+ if (g[CROP_MODE_FLAG]) return false;
4206
+ const canvas = g.canvas;
4207
+ if (!canvas) return false;
4208
+ g[CROP_MODE_FLAG] = true;
4209
+ const innerImg = ct._img;
4210
+ const imgEl = innerImg._element;
4211
+ if (!imgEl) {
4212
+ g[CROP_MODE_FLAG] = false;
4213
+ return false;
4214
+ }
4215
+ const ghost = new fabric__namespace.FabricImage(imgEl, {
4216
+ selectable: false,
4217
+ evented: false,
4218
+ hasControls: false,
4219
+ hasBorders: false,
4220
+ opacity: 0.35,
4221
+ originX: "center",
4222
+ originY: "center",
4223
+ objectCaching: false
4224
+ });
4225
+ ghost.excludeFromExport = true;
4226
+ ghost[CROP_GHOST_FLAG] = true;
4227
+ const outlineShape = ct.shape;
4228
+ const outline = outlineShape === "circle" ? new fabric__namespace.Ellipse({
4229
+ rx: ct.frameW / 2,
4230
+ ry: ct.frameH / 2,
4231
+ fill: "transparent",
4232
+ stroke: "hsl(256, 80%, 58%)",
4233
+ strokeWidth: 1.5,
4234
+ strokeDashArray: [6, 4],
4235
+ strokeUniform: true,
4236
+ selectable: false,
4237
+ evented: false,
4238
+ hasControls: false,
4239
+ hasBorders: false,
4240
+ originX: "center",
4241
+ originY: "center",
4242
+ objectCaching: false
4243
+ }) : new fabric__namespace.Rect({
4244
+ width: ct.frameW,
4245
+ height: ct.frameH,
4246
+ fill: "transparent",
4247
+ stroke: "hsl(256, 80%, 58%)",
4248
+ strokeWidth: 1.5,
4249
+ strokeDashArray: [6, 4],
4250
+ strokeUniform: true,
4251
+ selectable: false,
4252
+ evented: false,
4253
+ hasControls: false,
4254
+ hasBorders: false,
4255
+ originX: "center",
4256
+ originY: "center",
4257
+ objectCaching: false
4258
+ });
4259
+ outline.excludeFromExport = true;
4260
+ outline[CROP_OUTLINE_FLAG] = true;
4261
+ g[CROP_GHOST_FLAG] = ghost;
4262
+ g[CROP_OUTLINE_FLAG] = outline;
4263
+ const groupIndex = canvas._objects.indexOf(g);
4264
+ if (groupIndex >= 0) {
4265
+ canvas.insertAt(groupIndex, ghost);
4266
+ canvas.add(outline);
4267
+ canvas.bringObjectToFront(g);
4268
+ canvas.bringObjectToFront(outline);
4269
+ } else {
4270
+ canvas.add(ghost);
4271
+ canvas.add(g);
4272
+ canvas.add(outline);
4273
+ }
4274
+ syncGhostTransform(g);
4275
+ syncOutlineTransform(g);
4276
+ const groupAnchor = { left: g.left ?? 0, top: g.top ?? 0 };
4277
+ let panRafId = null;
4278
+ let pendingDx = 0;
4279
+ let pendingDy = 0;
4280
+ let lastDragDx = 0;
4281
+ let lastDragDy = 0;
4282
+ const flushPan = () => {
4283
+ panRafId = null;
4284
+ if (pendingDx === 0 && pendingDy === 0) return;
4285
+ const innerCt = g.__cropData;
4286
+ const img = innerCt == null ? void 0 : innerCt._img;
4287
+ if (!img) {
4288
+ pendingDx = 0;
4289
+ pendingDy = 0;
4290
+ return;
4291
+ }
4292
+ const angle = g.angle ?? 0;
4293
+ const rad = fabric__namespace.util.degreesToRadians(-angle);
4294
+ const cos = Math.cos(rad);
4295
+ const sin = Math.sin(rad);
4296
+ const localDx = pendingDx * cos - pendingDy * sin;
4297
+ const localDy = pendingDx * sin + pendingDy * cos;
4298
+ pendingDx = 0;
4299
+ pendingDy = 0;
4300
+ const iw = img.width || 1;
4301
+ const ih = img.height || 1;
4302
+ const finalScale = img.scaleX || 1;
4303
+ const dispW = iw * finalScale;
4304
+ const dispH = ih * finalScale;
4305
+ const overflowX = Math.max(0, dispW - innerCt.frameW);
4306
+ const overflowY = Math.max(0, dispH - innerCt.frameH);
4307
+ img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
4308
+ const ct2 = img._ct;
4309
+ if (overflowX > 0) ct2.panX = clamp((ct2.panX ?? 0.5) - localDx / overflowX, 0, 1);
4310
+ if (overflowY > 0) ct2.panY = clamp((ct2.panY ?? 0.5) - localDy / overflowY, 0, 1);
4311
+ updateCoverLayout(g);
4312
+ syncGhostTransform(g);
4313
+ g.setCoords();
4314
+ canvas.requestRenderAll();
4315
+ };
4316
+ const onMoving = (opt) => {
4317
+ const target = opt == null ? void 0 : opt.target;
4318
+ if (target !== g) return;
4319
+ const rawDx = (g.left ?? 0) - groupAnchor.left;
4320
+ const rawDy = (g.top ?? 0) - groupAnchor.top;
4321
+ const dx = rawDx - lastDragDx;
4322
+ const dy = rawDy - lastDragDy;
4323
+ lastDragDx = rawDx;
4324
+ lastDragDy = rawDy;
4325
+ g.left = groupAnchor.left;
4326
+ g.top = groupAnchor.top;
4327
+ if (dx === 0 && dy === 0) return;
4328
+ pendingDx += dx;
4329
+ pendingDy += dy;
4330
+ if (panRafId == null) panRafId = requestAnimationFrame(flushPan);
4331
+ };
4332
+ const onModified = (opt) => {
4333
+ const target = opt == null ? void 0 : opt.target;
4334
+ if (target !== g) return;
4335
+ if (panRafId != null) {
4336
+ cancelAnimationFrame(panRafId);
4337
+ panRafId = null;
4338
+ flushPan();
4339
+ }
4340
+ lastDragDx = 0;
4341
+ lastDragDy = 0;
4342
+ g.left = groupAnchor.left;
4343
+ g.top = groupAnchor.top;
4344
+ g.setCoords();
4345
+ };
4346
+ const onCanvasMouseDown = (opt) => {
4347
+ const target = opt.target;
4348
+ if (target === g || target === ghost || target === outline) return;
4349
+ setTimeout(() => exitCropMode(g, true), 0);
4350
+ };
4351
+ const onDblClick = (opt) => {
4352
+ const target = opt == null ? void 0 : opt.target;
4353
+ if (target !== g && target !== ghost && target !== outline) return;
4354
+ exitCropMode(g, true);
4355
+ };
4356
+ const onKeyDown = (e) => {
4357
+ if (e.key === "Escape" || e.key === "Enter") {
4358
+ e.preventDefault();
4359
+ e.stopPropagation();
4360
+ exitCropMode(g, true);
4361
+ }
4362
+ };
4363
+ let wheelCommitTimer = null;
4364
+ let zoomRafId = null;
4365
+ let pendingZoomDelta = 0;
4366
+ let pendingZoomCenter = null;
4367
+ let lastWheelWasPinch = false;
4368
+ const flushZoom = () => {
4369
+ zoomRafId = null;
4370
+ if (pendingZoomDelta === 0 || !pendingZoomCenter) return;
4371
+ const innerCt = g.__cropData;
4372
+ const img = innerCt == null ? void 0 : innerCt._img;
4373
+ if (!img) {
4374
+ pendingZoomDelta = 0;
4375
+ pendingZoomCenter = null;
4376
+ return;
4377
+ }
4378
+ const factor = lastWheelWasPinch ? 0.012 : 22e-4;
4379
+ const ratio = Math.exp(-pendingZoomDelta * factor);
4380
+ pendingZoomDelta = 0;
4381
+ img._ct = img._ct || { panX: 0.5, panY: 0.5, zoom: 1 };
4382
+ const ct2 = img._ct;
4383
+ const prevZoom = ct2.zoom ?? 1;
4384
+ const nextZoom = clamp(prevZoom * ratio, 1, 8);
4385
+ if (nextZoom === prevZoom) {
4386
+ pendingZoomCenter = null;
4387
+ return;
4388
+ }
4389
+ const angle = g.angle ?? 0;
4390
+ const rad = fabric__namespace.util.degreesToRadians(-angle);
4391
+ const cos = Math.cos(rad);
4392
+ const sin = Math.sin(rad);
4393
+ const cx = g.left ?? 0;
4394
+ const cy = g.top ?? 0;
4395
+ const wx = pendingZoomCenter.x - cx;
4396
+ const wy = pendingZoomCenter.y - cy;
4397
+ const localCx = wx * cos - wy * sin;
4398
+ const localCy = wx * sin + wy * cos;
4399
+ const iw = img.width || 1;
4400
+ const ih = img.height || 1;
4401
+ const prevScale = img.scaleX || 1;
4402
+ const prevOverflowX = Math.max(0, iw * prevScale - innerCt.frameW);
4403
+ const prevOverflowY = Math.max(0, ih * prevScale - innerCt.frameH);
4404
+ const prevPanX = ct2.panX ?? 0.5;
4405
+ const prevPanY = ct2.panY ?? 0.5;
4406
+ const prevOffsetX = prevOverflowX > 0 ? -prevOverflowX * (prevPanX - 0.5) : 0;
4407
+ const prevOffsetY = prevOverflowY > 0 ? -prevOverflowY * (prevPanY - 0.5) : 0;
4408
+ const imgPxX = (localCx - prevOffsetX) / prevScale + iw / 2;
4409
+ const imgPxY = (localCy - prevOffsetY) / prevScale + ih / 2;
4410
+ ct2.zoom = nextZoom;
4411
+ updateCoverLayout(g);
4412
+ const newScale = img.scaleX || 1;
4413
+ const newOverflowX = Math.max(0, iw * newScale - innerCt.frameW);
4414
+ const newOverflowY = Math.max(0, ih * newScale - innerCt.frameH);
4415
+ if (newOverflowX > 0) {
4416
+ const desiredOffsetX = localCx - (imgPxX - iw / 2) * newScale;
4417
+ ct2.panX = clamp(0.5 - desiredOffsetX / newOverflowX, 0, 1);
4418
+ } else {
4419
+ ct2.panX = 0.5;
4420
+ }
4421
+ if (newOverflowY > 0) {
4422
+ const desiredOffsetY = localCy - (imgPxY - ih / 2) * newScale;
4423
+ ct2.panY = clamp(0.5 - desiredOffsetY / newOverflowY, 0, 1);
4424
+ } else {
4425
+ ct2.panY = 0.5;
4426
+ }
4427
+ updateCoverLayout(g);
4428
+ syncGhostTransform(g);
4429
+ g.setCoords();
4430
+ canvas.requestRenderAll();
4431
+ if (wheelCommitTimer) clearTimeout(wheelCommitTimer);
4432
+ wheelCommitTimer = setTimeout(() => {
4433
+ canvas.fire("object:modified", { target: g });
4434
+ g.left = groupAnchor.left;
4435
+ g.top = groupAnchor.top;
4436
+ g.setCoords();
4437
+ }, 200);
4438
+ pendingZoomCenter = null;
4439
+ };
4440
+ const onWheel = (e) => {
4441
+ if (!canvas) return;
4442
+ const innerCt = g.__cropData;
4443
+ if (!innerCt) return;
4444
+ const pointer = canvas.getPointer(e, false);
4445
+ const br = g.getBoundingRect();
4446
+ if (pointer.x < br.left || pointer.x > br.left + br.width || pointer.y < br.top || pointer.y > br.top + br.height) return;
4447
+ const img = innerCt._img;
4448
+ if (!img) return;
4449
+ e.preventDefault();
4450
+ e.stopPropagation();
4451
+ pendingZoomDelta += e.deltaY;
4452
+ pendingZoomCenter = pointer;
4453
+ lastWheelWasPinch = !!e.ctrlKey;
4454
+ if (zoomRafId == null) zoomRafId = requestAnimationFrame(flushZoom);
4455
+ };
4456
+ canvas.on("object:moving", onMoving);
4457
+ canvas.on("object:modified", onModified);
4458
+ canvas.on("mouse:down:before", onCanvasMouseDown);
4459
+ canvas.on("mouse:dblclick", onDblClick);
4460
+ window.addEventListener("keydown", onKeyDown, true);
4461
+ const upperCanvasEl = canvas.upperCanvasEl;
4462
+ upperCanvasEl == null ? void 0 : upperCanvasEl.addEventListener("wheel", onWheel, { passive: false });
4463
+ g[CROP_HANDLERS_FLAG] = {
4464
+ onMoving,
4465
+ onModified,
4466
+ onCanvasMouseDown,
4467
+ onDblClick,
4468
+ onKeyDown,
4469
+ onWheel,
4470
+ upperCanvasEl
4471
+ };
4472
+ installCropModeVisuals(g);
4473
+ canvas.setActiveObject(g);
4474
+ canvas.requestRenderAll();
4475
+ return true;
4476
+ }
4477
+ function exitCropMode(g, commit = true) {
4478
+ var _a;
4479
+ if (!g[CROP_MODE_FLAG]) return;
4480
+ const canvas = g.canvas;
4481
+ const handlers = g[CROP_HANDLERS_FLAG];
4482
+ if (handlers && canvas) {
4483
+ canvas.off("object:moving", handlers.onMoving);
4484
+ canvas.off("object:modified", handlers.onModified);
4485
+ canvas.off("mouse:down:before", handlers.onCanvasMouseDown);
4486
+ if (handlers.onDblClick) canvas.off("mouse:dblclick", handlers.onDblClick);
4487
+ window.removeEventListener("keydown", handlers.onKeyDown, true);
4488
+ (_a = handlers.upperCanvasEl) == null ? void 0 : _a.removeEventListener("wheel", handlers.onWheel);
4489
+ }
4490
+ g[CROP_HANDLERS_FLAG] = void 0;
4491
+ const ghost = g[CROP_GHOST_FLAG];
4492
+ const outline = g[CROP_OUTLINE_FLAG];
4493
+ if (canvas && ghost) canvas.remove(ghost);
4494
+ if (canvas && outline) canvas.remove(outline);
4495
+ g[CROP_GHOST_FLAG] = void 0;
4496
+ g[CROP_OUTLINE_FLAG] = void 0;
4497
+ g.lockMovementX = false;
4498
+ g.lockMovementY = false;
4499
+ g.lockRotation = false;
4500
+ g.lockScalingX = false;
4501
+ g.lockScalingY = false;
4502
+ g.hasControls = true;
4503
+ g.borderDashArray = void 0;
4504
+ installCanvaMaskControls(g);
4505
+ g.borderColor = void 0;
4506
+ g.cornerColor = void 0;
4507
+ g.cornerStrokeColor = void 0;
4508
+ g.cornerStyle = "rect";
4509
+ g[CROP_MODE_FLAG] = false;
4510
+ g.__cropZoomLastPointer = void 0;
4511
+ g.__lastPointerForCrop = void 0;
4512
+ if (commit && canvas) {
4513
+ canvas.fire("object:modified", { target: g });
4514
+ }
4515
+ canvas == null ? void 0 : canvas.requestRenderAll();
4516
+ }
3725
4517
  function getObjectSnapPoints(obj) {
3726
4518
  try {
3727
4519
  obj.setCoords();
@@ -5290,13 +6082,7 @@ const PageCanvas = react.forwardRef(
5290
6082
  const activeObj = fabricCanvas.getActiveObject();
5291
6083
  if (activeObj) applyControlSizeForZoom(fabricCanvas, activeObj);
5292
6084
  if (activeObj && !(activeObj instanceof fabric__namespace.ActiveSelection) && (((_a = activeObj._ct) == null ? void 0 : _a.isCropGroup) || activeObj.__cropGroup)) {
5293
- const objId = getObjectId(activeObj);
5294
- const element = elementsRef.current.find((el) => el.id === objId);
5295
- if (element == null ? void 0 : element.useCropHandles) {
5296
- installCanvaMaskControls(activeObj);
5297
- } else {
5298
- setSimpleScaleControls(activeObj);
5299
- }
6085
+ installCanvaMaskControls(activeObj);
5300
6086
  }
5301
6087
  });
5302
6088
  fabricCanvas.on("selection:updated", () => {
@@ -5305,13 +6091,7 @@ const PageCanvas = react.forwardRef(
5305
6091
  const activeObj = fabricCanvas.getActiveObject();
5306
6092
  if (activeObj) applyControlSizeForZoom(fabricCanvas, activeObj);
5307
6093
  if (activeObj && !(activeObj instanceof fabric__namespace.ActiveSelection) && (((_a = activeObj._ct) == null ? void 0 : _a.isCropGroup) || activeObj.__cropGroup)) {
5308
- const objId = getObjectId(activeObj);
5309
- const element = elementsRef.current.find((el) => el.id === objId);
5310
- if (element == null ? void 0 : element.useCropHandles) {
5311
- installCanvaMaskControls(activeObj);
5312
- } else {
5313
- setSimpleScaleControls(activeObj);
5314
- }
6094
+ installCanvaMaskControls(activeObj);
5315
6095
  }
5316
6096
  });
5317
6097
  fabricCanvas.on("selection:cleared", () => {
@@ -5329,6 +6109,8 @@ const PageCanvas = react.forwardRef(
5329
6109
  let dragStarted = false;
5330
6110
  const markTransforming = (target) => {
5331
6111
  if (!target) return;
6112
+ const targetId = getObjectId(target);
6113
+ if (targetId) transformingIdsRef.current.add(targetId);
5332
6114
  if (target instanceof fabric__namespace.Group) {
5333
6115
  target.getObjects().forEach((obj) => {
5334
6116
  const id2 = getObjectId(obj);
@@ -5349,7 +6131,7 @@ const PageCanvas = react.forwardRef(
5349
6131
  const clearTransforming = () => {
5350
6132
  transformingIdsRef.current.clear();
5351
6133
  };
5352
- const isCropGroup = (o) => {
6134
+ const isCropGroup2 = (o) => {
5353
6135
  var _a;
5354
6136
  return !!(((_a = o == null ? void 0 : o._ct) == null ? void 0 : _a.isCropGroup) || (o == null ? void 0 : o.__cropGroup));
5355
6137
  };
@@ -5357,9 +6139,9 @@ const PageCanvas = react.forwardRef(
5357
6139
  var _a, _b, _c;
5358
6140
  const t = opt.target;
5359
6141
  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") {
5360
- if (t && isCropGroup(t)) {
6142
+ if (t && isCropGroup2(t)) {
5361
6143
  fabricCanvas._hoveredTarget = t;
5362
- } else if ((t == null ? void 0 : t.group) && isCropGroup(t.group)) {
6144
+ } else if ((t == null ? void 0 : t.group) && isCropGroup2(t.group)) {
5363
6145
  fabricCanvas._hoveredTarget = t.group;
5364
6146
  }
5365
6147
  return;
@@ -5368,7 +6150,7 @@ const PageCanvas = react.forwardRef(
5368
6150
  const pointer = fabricCanvas.getPointer(opt.e);
5369
6151
  const objects = fabricCanvas.getObjects();
5370
6152
  for (const obj of objects) {
5371
- if (isCropGroup(obj) && obj.containsPoint(pointer)) {
6153
+ if (isCropGroup2(obj) && obj.containsPoint(pointer)) {
5372
6154
  fabricCanvas.setActiveObject(obj);
5373
6155
  opt.target = obj;
5374
6156
  fabricCanvas._hoveredTarget = obj;
@@ -5378,13 +6160,13 @@ const PageCanvas = react.forwardRef(
5378
6160
  return;
5379
6161
  }
5380
6162
  const g = t.group;
5381
- if (g && isCropGroup(g)) {
6163
+ if (g && isCropGroup2(g)) {
5382
6164
  fabricCanvas.setActiveObject(g);
5383
6165
  opt.target = g;
5384
6166
  fabricCanvas._hoveredTarget = g;
5385
6167
  return;
5386
6168
  }
5387
- if (isCropGroup(t)) {
6169
+ if (isCropGroup2(t)) {
5388
6170
  fabricCanvas.setActiveObject(t);
5389
6171
  t.set({
5390
6172
  selectable: true,
@@ -5927,8 +6709,8 @@ const PageCanvas = react.forwardRef(
5927
6709
  const h = (active.height ?? 0) * (active.scaleY ?? 1);
5928
6710
  const centerX = active.left ?? 0;
5929
6711
  const centerY = active.top ?? 0;
5930
- const groupLeft = centerX - w / 2;
5931
- const groupTop = centerY - h / 2;
6712
+ const groupLeft = active.originX === "center" ? centerX - w / 2 : centerX;
6713
+ const groupTop = active.originY === "center" ? centerY - h / 2 : centerY;
5932
6714
  const storePos = absoluteToStorePosition(groupLeft, groupTop, groupId, pageChildren2);
5933
6715
  useEditorStore.getState().updateNode(groupId, { left: storePos.left, top: storePos.top }, { recordHistory: false, skipLayoutRecalc: true });
5934
6716
  commitHistory();
@@ -5961,76 +6743,79 @@ const PageCanvas = react.forwardRef(
5961
6743
  const commonAncestor = findCommonAncestorGroup(selectedElementIds, pageChildren2);
5962
6744
  const candidateGroup = sameDirectParent ? parentGroups[0] : commonAncestor;
5963
6745
  const candidateIsStack = candidateGroup && isStackLayoutMode(candidateGroup.layoutMode);
5964
- const groupToMove = candidateIsStack ? candidateGroup : null;
5965
- if (groupToMove && firstId) {
5966
- const firstNode = findNodeById(pageChildren2, firstId);
5967
- if (firstNode) {
5968
- const origAbs = getAbsoluteBounds(firstNode, pageChildren2);
5969
- let absoluteLeft;
5970
- let absoluteTop;
5971
- if (isActiveSelection && activeObj) {
5972
- const selectionMatrix = activeObj.calcTransformMatrix();
5973
- const relativePoint = { x: firstObj.left ?? 0, y: firstObj.top ?? 0 };
5974
- const absolutePoint = fabric__namespace.util.transformPoint(relativePoint, selectionMatrix);
5975
- absoluteLeft = absolutePoint.x;
5976
- absoluteTop = absolutePoint.y;
5977
- } else {
5978
- absoluteLeft = firstObj.left ?? 0;
5979
- absoluteTop = firstObj.top ?? 0;
6746
+ const allMembersSelected = (() => {
6747
+ if (!candidateGroup) return false;
6748
+ const memberIds = getAllElementIds(candidateGroup.children ?? []);
6749
+ if (memberIds.length === 0) return false;
6750
+ const selectedSet = new Set(selectedElementIds);
6751
+ return memberIds.every((mid) => selectedSet.has(mid));
6752
+ })();
6753
+ const groupToMove = candidateIsStack || allMembersSelected ? candidateGroup : null;
6754
+ if (groupToMove) {
6755
+ const groupAbs = getAbsoluteBounds(groupToMove, pageChildren2);
6756
+ let movedGroupLeft = groupAbs.left;
6757
+ let movedGroupTop = groupAbs.top;
6758
+ if (activeObj) {
6759
+ const selectionRect = activeObj.getBoundingRect();
6760
+ movedGroupLeft = selectionRect.left;
6761
+ movedGroupTop = selectionRect.top;
6762
+ } else if (firstId) {
6763
+ const firstNode = findNodeById(pageChildren2, firstId);
6764
+ if (firstNode) {
6765
+ movedGroupLeft = getAbsoluteBounds(firstNode, pageChildren2).left;
6766
+ movedGroupTop = getAbsoluteBounds(firstNode, pageChildren2).top;
5980
6767
  }
5981
- const deltaX = absoluteLeft - origAbs.left;
5982
- const deltaY = absoluteTop - origAbs.top;
5983
- const hadScale = isActiveSelection && activeObj && (Math.abs((activeObj.scaleX ?? 1) - 1) > 0.01 || Math.abs((activeObj.scaleY ?? 1) - 1) > 0.01);
5984
- if (!hadScale && (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1)) {
5985
- if (typeof console !== "undefined" && console.log) {
5986
- console.log("[object:modified] plain-groups: moving group id=", groupToMove.id, "newLeft=", (groupToMove.left ?? 0) + deltaX, "newTop=", (groupToMove.top ?? 0) + deltaY);
5987
- }
5988
- const { updateNode: updateNodeStore, commitHistory: commitHistoryStore, getCurrentElements } = useEditorStore.getState();
5989
- const newLeft = (groupToMove.left ?? 0) + deltaX;
5990
- const newTop = (groupToMove.top ?? 0) + deltaY;
5991
- updateNodeStore(groupToMove.id, { left: newLeft, top: newTop }, { recordHistory: false, skipLayoutRecalc: true });
5992
- commitHistoryStore();
5993
- if (isActiveSelection && activeObj instanceof fabric__namespace.ActiveSelection) {
5994
- skipSelectionClearOnDiscardRef.current = true;
5995
- fabricCanvas.discardActiveObject();
5996
- }
5997
- const stateAfter = useEditorStore.getState();
5998
- const pageAfter = stateAfter.canvas.pages.find((p) => p.id === pageId);
5999
- const pageChildrenAfter = (pageAfter == null ? void 0 : pageAfter.children) ?? [];
6000
- for (const obj of activeObjects) {
6001
- const objId = getObjectId(obj);
6002
- if (!objId || objId === "__background__") continue;
6003
- const node = findNodeById(pageChildrenAfter, objId);
6004
- if (node) {
6005
- const abs = getAbsoluteBounds(node, pageChildrenAfter);
6006
- if (obj instanceof fabric__namespace.Group && obj.__cropGroup) {
6007
- const w = (obj.width ?? 0) * (obj.scaleX ?? 1);
6008
- const h = (obj.height ?? 0) * (obj.scaleY ?? 1);
6009
- obj.set({ left: abs.left + w / 2, top: abs.top + h / 2 });
6010
- } else {
6011
- obj.set({ left: abs.left, top: abs.top });
6012
- }
6013
- obj.setCoords();
6014
- }
6015
- }
6016
- if (isActiveSelection && activeObjects.length > 0) {
6017
- const selection = new fabric__namespace.ActiveSelection([...activeObjects], { canvas: fabricCanvas });
6018
- fabricCanvas.setActiveObject(selection);
6019
- skipSelectionClearOnDiscardRef.current = false;
6768
+ }
6769
+ const deltaX = movedGroupLeft - groupAbs.left;
6770
+ const deltaY = movedGroupTop - groupAbs.top;
6771
+ const hadScale = isActiveSelection && activeObj && (Math.abs((activeObj.scaleX ?? 1) - 1) > 0.01 || Math.abs((activeObj.scaleY ?? 1) - 1) > 0.01);
6772
+ if (!hadScale && (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1)) {
6773
+ if (typeof console !== "undefined" && console.log) {
6774
+ console.log("[object:modified] plain-groups: moving group id=", groupToMove.id, "newLeft=", (groupToMove.left ?? 0) + deltaX, "newTop=", (groupToMove.top ?? 0) + deltaY);
6775
+ }
6776
+ const { updateNode: updateNodeStore, commitHistory: commitHistoryStore, getCurrentElements } = useEditorStore.getState();
6777
+ const newLeft = (groupToMove.left ?? 0) + deltaX;
6778
+ const newTop = (groupToMove.top ?? 0) + deltaY;
6779
+ updateNodeStore(groupToMove.id, { left: newLeft, top: newTop }, { recordHistory: false, skipLayoutRecalc: true });
6780
+ commitHistoryStore();
6781
+ if (isActiveSelection && activeObj instanceof fabric__namespace.ActiveSelection) {
6782
+ skipSelectionClearOnDiscardRef.current = true;
6783
+ fabricCanvas.discardActiveObject();
6784
+ }
6785
+ const stateAfter = useEditorStore.getState();
6786
+ const pageAfter = stateAfter.canvas.pages.find((p) => p.id === pageId);
6787
+ const pageChildrenAfter = (pageAfter == null ? void 0 : pageAfter.children) ?? [];
6788
+ for (const obj of activeObjects) {
6789
+ const objId = getObjectId(obj);
6790
+ if (!objId || objId === "__background__") continue;
6791
+ const node = findNodeById(pageChildrenAfter, objId);
6792
+ if (node) {
6793
+ const abs = getAbsoluteBounds(node, pageChildrenAfter);
6794
+ const w = (obj.width ?? 0) * (obj.scaleX ?? 1);
6795
+ const h = (obj.height ?? 0) * (obj.scaleY ?? 1);
6796
+ const nextLeft = obj.originX === "center" ? abs.left + w / 2 : abs.left;
6797
+ const nextTop = obj.originY === "center" ? abs.top + h / 2 : abs.top;
6798
+ obj.set({ left: nextLeft, top: nextTop });
6799
+ obj.setCoords();
6020
6800
  }
6021
- fabricCanvas.requestRenderAll();
6022
- elementsRef.current = getCurrentElements();
6023
- for (const obj of activeObjects) {
6024
- const objId = getObjectId(obj);
6025
- if (objId && objId !== "__background__") {
6026
- justModifiedIdsRef.current.add(objId);
6027
- modifiedIdsThisRound.add(objId);
6028
- }
6801
+ }
6802
+ if (isActiveSelection && activeObjects.length > 0) {
6803
+ const selection = new fabric__namespace.ActiveSelection([...activeObjects], { canvas: fabricCanvas });
6804
+ fabricCanvas.setActiveObject(selection);
6805
+ skipSelectionClearOnDiscardRef.current = false;
6806
+ }
6807
+ fabricCanvas.requestRenderAll();
6808
+ elementsRef.current = getCurrentElements();
6809
+ for (const obj of activeObjects) {
6810
+ const objId = getObjectId(obj);
6811
+ if (objId && objId !== "__background__") {
6812
+ justModifiedIdsRef.current.add(objId);
6813
+ modifiedIdsThisRound.add(objId);
6029
6814
  }
6030
- setTimeout(() => modifiedIdsThisRound.forEach((id) => justModifiedIdsRef.current.delete(id)), 150);
6031
- unlockEditsSoon();
6032
- return;
6033
6815
  }
6816
+ setTimeout(() => modifiedIdsThisRound.forEach((id) => justModifiedIdsRef.current.delete(id)), 150);
6817
+ unlockEditsSoon();
6818
+ return;
6034
6819
  }
6035
6820
  }
6036
6821
  }
@@ -6109,9 +6894,13 @@ const PageCanvas = react.forwardRef(
6109
6894
  finalScaleX = 1;
6110
6895
  finalScaleY = 1;
6111
6896
  obj.set({ scaleX: 1, scaleY: 1 });
6112
- updateCoverLayout(obj);
6113
- obj.__lastResizeHandle = null;
6114
- fabricCanvas.setActiveObject(obj);
6897
+ if (!isActiveSelection) {
6898
+ updateCoverLayout(obj);
6899
+ obj.__lastResizeHandle = null;
6900
+ fabricCanvas.setActiveObject(obj);
6901
+ } else {
6902
+ obj.__lastResizeHandle = null;
6903
+ }
6115
6904
  }
6116
6905
  } else if (obj instanceof fabric__namespace.FabricImage) {
6117
6906
  if ((sourceElement == null ? void 0 : sourceElement.smartElementType) && sourceElement.smartProps) {
@@ -6254,6 +7043,13 @@ const PageCanvas = react.forwardRef(
6254
7043
  fabricCanvas.on("mouse:dblclick", (e) => {
6255
7044
  if (!isActiveRef.current || !allowEditing) return;
6256
7045
  let target = e.target;
7046
+ if (target && target instanceof fabric__namespace.Group && target.__cropGroup) {
7047
+ const ct = target.__cropData;
7048
+ if ((ct == null ? void 0 : ct._img) && !isCropGroupInCropMode(target)) {
7049
+ enterCropMode(target);
7050
+ return;
7051
+ }
7052
+ }
6257
7053
  if (!target || !(target instanceof fabric__namespace.Textbox)) {
6258
7054
  const active = fabricCanvas.getActiveObject();
6259
7055
  if (active instanceof fabric__namespace.Textbox) target = active;
@@ -6446,9 +7242,9 @@ const PageCanvas = react.forwardRef(
6446
7242
  const activeId = activeBeforeSync ? getObjectId(activeBeforeSync) : null;
6447
7243
  const isActiveTextBeingEdited = activeId && editingTextIdRef.current === activeId && activeBeforeSync instanceof fabric__namespace.Textbox;
6448
7244
  if (!skipRestoreSelection && activeBeforeSync && activeStillOnCanvas && !isActiveTextBeingEdited) {
6449
- const isCropGroup = ((_d = activeBeforeSync._ct) == null ? void 0 : _d.isCropGroup) || activeBeforeSync.__cropGroup;
7245
+ const isCropGroup2 = ((_d = activeBeforeSync._ct) == null ? void 0 : _d.isCropGroup) || activeBeforeSync.__cropGroup;
6450
7246
  const isSectionGroup = activeId && sectionGroupIds.has(activeId);
6451
- if ((isCropGroup || !shouldSkipUpdates2) && !isSectionGroup) {
7247
+ if ((isCropGroup2 || !shouldSkipUpdates2) && !isSectionGroup) {
6452
7248
  fc.setActiveObject(activeBeforeSync);
6453
7249
  }
6454
7250
  }
@@ -6569,11 +7365,12 @@ const PageCanvas = react.forwardRef(
6569
7365
  const sourceUrlChanged = currentUrlNormalized !== storedUrlNormalized;
6570
7366
  const needsReload = sourceUrlChanged || colorMapChanged;
6571
7367
  const hasUrl = currentUrlNormalized !== "";
6572
- const isCropGroup = existingObj instanceof fabric__namespace.Group && existingObj.__cropGroup;
6573
- const isPlaceholder = existingObj instanceof fabric__namespace.Group && !isCropGroup || !(existingObj instanceof fabric__namespace.FabricImage);
7368
+ const isCropGroup2 = existingObj instanceof fabric__namespace.Group && existingObj.__cropGroup;
7369
+ const isPlaceholderGroup = isEmptyImagePlaceholderGroup(existingObj);
7370
+ const isPlaceholder = isPlaceholderGroup || !(existingObj instanceof fabric__namespace.FabricImage) || existingObj instanceof fabric__namespace.Group && !isCropGroup2;
6574
7371
  const hadUrlBefore = storedImageUrl && String(storedImageUrl).trim() !== "";
6575
7372
  if (!hasUrl && hadUrlBefore) {
6576
- const placeholder = isCropGroup ? createImagePlaceholderForGroup(element) : createImagePlaceholder(element);
7373
+ const placeholder = isCropGroup2 ? createImagePlaceholderForGroup(element) : createImagePlaceholder(element);
6577
7374
  setObjectData(placeholder, element.id);
6578
7375
  placeholder.__imageSrc = "";
6579
7376
  const idx = fc.getObjects().indexOf(existingObj);
@@ -6589,13 +7386,13 @@ const PageCanvas = react.forwardRef(
6589
7386
  const imageFitForReplace = element.imageFit || ((_e = element.style) == null ? void 0 : _e.imageFit) || "cover";
6590
7387
  const clipShapeForReplace = element.clipShape ?? ((_f = element.style) == null ? void 0 : _f.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
6591
7388
  const needCropGroupForElement = imageFitForReplace !== "fill" || clipShapeForReplace && clipShapeForReplace !== "none";
6592
- const plainImageNeedsCropGroup = hasUrl && !isCropGroup && existingObj instanceof fabric__namespace.FabricImage && needCropGroupForElement;
7389
+ const plainImageNeedsCropGroup = hasUrl && !isCropGroup2 && existingObj instanceof fabric__namespace.FabricImage && needCropGroupForElement;
6593
7390
  if (hasUrl && (needsReload || isPlaceholder || plainImageNeedsCropGroup)) {
6594
7391
  if (needsReload && !isBeingTransformed && (!wasJustModified || sourceUrlChanged)) {
6595
7392
  loadImageAsync(element, existingObj, fc);
6596
7393
  } else if (plainImageNeedsCropGroup) {
6597
7394
  loadImageAsync(element, existingObj, fc);
6598
- } else if (!needsReload && isCropGroup) {
7395
+ } else if (!needsReload && isCropGroup2) {
6599
7396
  const ct = existingObj.__cropData;
6600
7397
  if (ct) {
6601
7398
  const resolvedCrop = pageTree.length > 0 ? getNodeBounds(element, pageTree) : { width: typeof element.width === "number" ? element.width : 200, height: typeof element.height === "number" ? element.height : 50 };
@@ -6641,6 +7438,22 @@ const PageCanvas = react.forwardRef(
6641
7438
  existingObj.set({ opacity: clampedOpacity });
6642
7439
  existingObj.set({ width: ct.frameW, height: ct.frameH });
6643
7440
  existingObj.set({ backgroundColor: "transparent" });
7441
+ const liveMask = existingObj.clipPath;
7442
+ const liveMaskUrl = existingObj.__svgMaskUrl;
7443
+ const liveMaskType = existingObj.__svgMaskType;
7444
+ const schemaMaskUrl = element.maskSvgUrl;
7445
+ const schemaMaskType = element.maskType || "shape";
7446
+ if (liveMask && liveMask.__svgMask && !schemaMaskUrl) {
7447
+ existingObj.clipPath = void 0;
7448
+ delete existingObj.__svgMaskUrl;
7449
+ delete existingObj.__svgMaskType;
7450
+ existingObj.dirty = true;
7451
+ } else if (schemaMaskUrl && (schemaMaskUrl !== liveMaskUrl || schemaMaskType !== liveMaskType)) {
7452
+ Promise.resolve().then(() => svgMaskApply).then(({ applyMaskToCropGroup: applyMaskToCropGroup2 }) => {
7453
+ applyMaskToCropGroup2(existingObj, schemaMaskUrl, schemaMaskType).catch(() => {
7454
+ });
7455
+ });
7456
+ }
6644
7457
  if (shapeChanged) {
6645
7458
  const needsNewClipPath = !existingObj.clipPath || newShape === "circle" && !(existingObj.clipPath instanceof fabric__namespace.Ellipse) || newShape !== "circle" && !(existingObj.clipPath instanceof fabric__namespace.Rect);
6646
7459
  if (needsNewClipPath) {
@@ -6677,11 +7490,7 @@ const PageCanvas = react.forwardRef(
6677
7490
  }
6678
7491
  updateCoverLayout(existingObj);
6679
7492
  if (allowEditing) {
6680
- if (element.useCropHandles) {
6681
- installCanvaMaskControls(existingObj);
6682
- } else {
6683
- setSimpleScaleControls(existingObj);
6684
- }
7493
+ installCanvaMaskControls(existingObj);
6685
7494
  } else {
6686
7495
  existingObj.set({
6687
7496
  hasControls: false,
@@ -6730,6 +7539,45 @@ const PageCanvas = react.forwardRef(
6730
7539
  }
6731
7540
  placeholder.__imageSrc = void 0;
6732
7541
  } else if (!isBeingTransformed && !isBeingTextEdited && !shouldSkipUpdates2) {
7542
+ if (isPlaceholderGroup) {
7543
+ const storePosImg = pageChildren ? (() => {
7544
+ const node = findNodeById(pageChildren, element.id);
7545
+ return node ? getAbsoluteBounds(node, pageChildren) : { left: element.left ?? 0, top: element.top ?? 0 };
7546
+ })() : { left: element.left ?? 0, top: element.top ?? 0 };
7547
+ 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 };
7548
+ const hasExplicitSize = typeof element.width === "number" && Number.isFinite(element.width) && element.width > 0 && typeof element.height === "number" && Number.isFinite(element.height) && element.height > 0;
7549
+ const minVisiblePlaceholder = hasExplicitSize ? 1 : 20;
7550
+ const nextWidth = Math.max(minVisiblePlaceholder, Number(resolvedSizeImg.width) || 200);
7551
+ const nextHeight = Math.max(minVisiblePlaceholder, Number(resolvedSizeImg.height) || 50);
7552
+ const isDynamicField = dynamicFieldIds.includes(element.id);
7553
+ const canBeEvented = isEditorMode || isPreviewMode && isDynamicField;
7554
+ updateEmptyPlaceholderLayout(existingObj, nextWidth, nextHeight);
7555
+ existingObj.set({
7556
+ left: storePosImg.left + nextWidth / 2,
7557
+ top: storePosImg.top + nextHeight / 2,
7558
+ originX: "center",
7559
+ originY: "center",
7560
+ angle: element.angle ?? 0,
7561
+ opacity: isHidden ? 0 : element.opacity ?? 1,
7562
+ flipX: element.flipX ?? false,
7563
+ flipY: element.flipY ?? false,
7564
+ selectable: allowSelection && !isHidden,
7565
+ evented: canBeEvented && !isHidden,
7566
+ hasControls: allowEditing && !isHidden,
7567
+ hasBorders: allowEditing && !isHidden,
7568
+ interactive: allowEditing && !isHidden,
7569
+ subTargetCheck: false,
7570
+ lockMovementX: !allowSelection,
7571
+ lockMovementY: !allowSelection,
7572
+ hoverCursor: isDynamicField && isPreviewMode ? "pointer" : void 0
7573
+ });
7574
+ if (allowEditing) {
7575
+ installCanvaMaskControls(existingObj);
7576
+ }
7577
+ existingObj.setCoords();
7578
+ fc.requestRenderAll();
7579
+ continue;
7580
+ }
6733
7581
  if (existingObj instanceof fabric__namespace.Group && existingObj.__cropGroup) {
6734
7582
  existingObj.set({
6735
7583
  flipX: element.flipX ?? false,
@@ -6904,7 +7752,14 @@ const PageCanvas = react.forwardRef(
6904
7752
  const node = findNodeById(pageTree, element.id);
6905
7753
  return node ? getAbsoluteBounds(node, pageTree) : { left: element.left ?? 0, top: element.top ?? 0 };
6906
7754
  })() : { left: element.left ?? 0, top: element.top ?? 0 };
6907
- placeholder.set({ left: absPosImg.left, top: absPosImg.top });
7755
+ const placeholderWidth = Number((placeholder.width ?? 0) * (placeholder.scaleX ?? 1));
7756
+ const placeholderHeight = Number((placeholder.height ?? 0) * (placeholder.scaleY ?? 1));
7757
+ placeholder.set({
7758
+ left: absPosImg.left + placeholderWidth / 2,
7759
+ top: absPosImg.top + placeholderHeight / 2,
7760
+ originX: "center",
7761
+ originY: "center"
7762
+ });
6908
7763
  placeholder.setCoords();
6909
7764
  const imageUrl = element.src || element.imageUrl;
6910
7765
  placeholder.__imageSrc = imageUrl;
@@ -6997,8 +7852,8 @@ const PageCanvas = react.forwardRef(
6997
7852
  isRebuildingRef.current = false;
6998
7853
  fc.requestRenderAll();
6999
7854
  if (activeBeforeSync && fc.getObjects().includes(activeBeforeSync)) {
7000
- const isCropGroup = ((_i = activeBeforeSync._ct) == null ? void 0 : _i.isCropGroup) || activeBeforeSync.__cropGroup;
7001
- if (isCropGroup) {
7855
+ const isCropGroup2 = ((_i = activeBeforeSync._ct) == null ? void 0 : _i.isCropGroup) || activeBeforeSync.__cropGroup;
7856
+ if (isCropGroup2) {
7002
7857
  fc.setActiveObject(activeBeforeSync);
7003
7858
  fc.requestRenderAll();
7004
7859
  }
@@ -7425,11 +8280,7 @@ const PageCanvas = react.forwardRef(
7425
8280
  obj.clipPath.dirty = true;
7426
8281
  obj.clipPath.setCoords();
7427
8282
  }
7428
- if (element == null ? void 0 : element.useCropHandles) {
7429
- installCanvaMaskControls(obj);
7430
- } else {
7431
- setSimpleScaleControls(obj);
7432
- }
8283
+ installCanvaMaskControls(obj);
7433
8284
  obj.set({
7434
8285
  selectable: true,
7435
8286
  evented: true,
@@ -8204,11 +9055,7 @@ const PageCanvas = react.forwardRef(
8204
9055
  evented: canBeEvented && !isHidden
8205
9056
  });
8206
9057
  } else {
8207
- if (element.useCropHandles) {
8208
- installCanvaMaskControls(cropGroup);
8209
- } else {
8210
- setSimpleScaleControls(cropGroup);
8211
- }
9058
+ installCanvaMaskControls(cropGroup);
8212
9059
  }
8213
9060
  const cropImg = (_l = cropGroup.__cropData) == null ? void 0 : _l._img;
8214
9061
  if (cropImg) {
@@ -8228,6 +9075,16 @@ const PageCanvas = react.forwardRef(
8228
9075
  useEditorStore.getState().updateElement(element.id, { imageNaturalWidth: nw, imageNaturalHeight: nh }, { recordHistory: false });
8229
9076
  }
8230
9077
  }
9078
+ const persistedMaskUrl = element.maskSvgUrl;
9079
+ if (persistedMaskUrl && typeof persistedMaskUrl === "string") {
9080
+ try {
9081
+ const { applyMaskToCropGroup: applyMaskToCropGroup2 } = await Promise.resolve().then(() => svgMaskApply);
9082
+ const persistedMaskType = element.maskType || "shape";
9083
+ await applyMaskToCropGroup2(cropGroup, persistedMaskUrl, persistedMaskType);
9084
+ } catch (err) {
9085
+ console.warn("[mask] failed to re-apply persisted mask", err);
9086
+ }
9087
+ }
8231
9088
  finalObject = cropGroup;
8232
9089
  } else {
8233
9090
  const isHiddenFill = !element.visible;
@@ -10516,11 +11373,11 @@ async function resolveTemplateData(options) {
10516
11373
  }
10517
11374
  async function resolveFromForm(options) {
10518
11375
  var _a, _b, _c;
10519
- const { templateId, formSchemaId, sectionState, themeId, supabaseUrl, supabaseAnonKey } = options;
11376
+ const { templateId, formSchemaId, sectionState, themeId, supabaseUrl, supabaseAnonKey, prefetched } = options;
10520
11377
  const [templateRow, formSchemaRow, defaultForm] = await Promise.all([
10521
- fetchRow(supabaseUrl, supabaseAnonKey, "templates", templateId),
10522
- fetchRow(supabaseUrl, supabaseAnonKey, "form_schemas", formSchemaId),
10523
- fetchDefaultForm(supabaseUrl, supabaseAnonKey, formSchemaId)
11378
+ (prefetched == null ? void 0 : prefetched.templateRow) ? Promise.resolve(prefetched.templateRow) : fetchRow(supabaseUrl, supabaseAnonKey, "templates", templateId),
11379
+ (prefetched == null ? void 0 : prefetched.formSchemaRow) !== void 0 ? Promise.resolve(prefetched.formSchemaRow) : fetchRow(supabaseUrl, supabaseAnonKey, "form_schemas", formSchemaId),
11380
+ (prefetched == null ? void 0 : prefetched.defaultForm) !== void 0 ? Promise.resolve(prefetched.defaultForm) : fetchDefaultForm(supabaseUrl, supabaseAnonKey, formSchemaId)
10524
11381
  ]);
10525
11382
  const templateConfig = templateRow.config;
10526
11383
  const templateFormSchema = templateRow.form_schema;
@@ -11133,14 +11990,15 @@ class PixldocsRenderer {
11133
11990
  * This is the primary external API for the package.
11134
11991
  */
11135
11992
  async renderFromForm(options) {
11136
- const { templateId, formSchemaId, sectionState, themeId, watermark, ...renderOpts } = options;
11993
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched, ...renderOpts } = options;
11137
11994
  const resolved = await resolveFromForm({
11138
11995
  templateId,
11139
11996
  formSchemaId,
11140
11997
  sectionState,
11141
11998
  themeId,
11142
11999
  supabaseUrl: this.config.supabaseUrl,
11143
- supabaseAnonKey: this.config.supabaseAnonKey
12000
+ supabaseAnonKey: this.config.supabaseAnonKey,
12001
+ prefetched
11144
12002
  });
11145
12003
  const shouldWatermark = watermark ?? resolved.price > 0;
11146
12004
  let configToRender = resolved.config;
@@ -11185,14 +12043,15 @@ class PixldocsRenderer {
11185
12043
  * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
11186
12044
  */
11187
12045
  async renderSvgsFromForm(options) {
11188
- const { templateId, formSchemaId, sectionState, themeId, watermark } = options;
12046
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched } = options;
11189
12047
  const resolved = await resolveFromForm({
11190
12048
  templateId,
11191
12049
  formSchemaId,
11192
12050
  sectionState,
11193
12051
  themeId,
11194
12052
  supabaseUrl: this.config.supabaseUrl,
11195
- supabaseAnonKey: this.config.supabaseAnonKey
12053
+ supabaseAnonKey: this.config.supabaseAnonKey,
12054
+ prefetched
11196
12055
  });
11197
12056
  const shouldWatermark = watermark ?? resolved.price > 0;
11198
12057
  let configToRender = resolved.config;
@@ -11216,14 +12075,15 @@ class PixldocsRenderer {
11216
12075
  * This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
11217
12076
  */
11218
12077
  async renderPdfFromForm(options) {
11219
- const { templateId, formSchemaId, sectionState, themeId, watermark, title, fontBaseUrl } = options;
12078
+ const { templateId, formSchemaId, sectionState, themeId, watermark, prefetched, title, fontBaseUrl } = options;
11220
12079
  const resolved = await resolveFromForm({
11221
12080
  templateId,
11222
12081
  formSchemaId,
11223
12082
  sectionState,
11224
12083
  themeId,
11225
12084
  supabaseUrl: this.config.supabaseUrl,
11226
- supabaseAnonKey: this.config.supabaseAnonKey
12085
+ supabaseAnonKey: this.config.supabaseAnonKey,
12086
+ prefetched
11227
12087
  });
11228
12088
  const shouldWatermark = watermark ?? resolved.price > 0;
11229
12089
  let configToRender = resolved.config;