vibrant-tool 0.1.7 → 0.1.9

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.
@@ -1075,9 +1075,9 @@
1075
1075
  function GlobalLoader() {
1076
1076
  const { isLoading, text } = useLoaderStore();
1077
1077
  if (!isLoading) return null;
1078
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-[999999] flex flex-col items-center justify-center bg-black/60 backdrop-blur-xs select-none", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3 bg-neutral-900/90 border border-neutral-800 px-6 py-5 rounded-2xl shadow-2xl", children: [
1079
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 w-9 animate-spin rounded-full border-3 border-[#02bc71] border-t-transparent" }),
1080
- text && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-neutral-200", children: text })
1078
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-[999999] flex flex-col items-center justify-center bg-black/65 backdrop-blur-[2px] select-none", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3.5 bg-neutral-900/95 border border-neutral-700/80 px-8 py-6 rounded-2xl shadow-2xl min-w-[240px] max-w-[360px] text-center", children: [
1079
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-10 w-10 animate-spin rounded-full border-3 border-[#02bc71] border-t-transparent shadow-sm" }) }),
1080
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-neutral-100 tracking-wide", children: text || "Please wait..." })
1081
1081
  ] }) });
1082
1082
  }
1083
1083
  function DownloadIcon() {
@@ -9027,7 +9027,7 @@
9027
9027
  waH,
9028
9028
  isVertical
9029
9029
  );
9030
- workarea.set({ rx, ry });
9030
+ workarea.set({ rx, ry, strokeWidth: 0, stroke: null });
9031
9031
  workarea.__radiusOfCanvas = radiusOfCanvas;
9032
9032
  workarea.__shapeType = shapeType;
9033
9033
  workarea.__isVertical = isVertical;
@@ -9046,8 +9046,61 @@
9046
9046
  top: (obj.top ?? 0) + dy
9047
9047
  });
9048
9048
  obj.setCoords();
9049
+ if (obj.clipPath) {
9050
+ obj.clipPath.set({
9051
+ left: (obj.clipPath.left ?? 0) + dx,
9052
+ top: (obj.clipPath.top ?? 0) + dy
9053
+ });
9054
+ obj.clipPath.setCoords();
9055
+ }
9049
9056
  }
9050
9057
  });
9058
+ if (workarea.clipPath) {
9059
+ workarea.clipPath.set({
9060
+ left: (workarea.clipPath.left ?? 0) + dx,
9061
+ top: (workarea.clipPath.top ?? 0) + dy
9062
+ });
9063
+ workarea.clipPath.setCoords();
9064
+ }
9065
+ if (canvas2.backgroundImage) {
9066
+ const bg = canvas2.backgroundImage;
9067
+ bg.set({
9068
+ left: (bg.left ?? 0) + dx,
9069
+ top: (bg.top ?? 0) + dy
9070
+ });
9071
+ bg.setCoords();
9072
+ if (bg.clipPath) {
9073
+ bg.clipPath.set({
9074
+ left: (bg.clipPath.left ?? 0) + dx,
9075
+ top: (bg.clipPath.top ?? 0) + dy
9076
+ });
9077
+ bg.clipPath.setCoords();
9078
+ }
9079
+ }
9080
+ }
9081
+ const bgImg = canvas2.getObjects().find((o2) => o2.name === "bg-image");
9082
+ if (bgImg) {
9083
+ const center = workarea.getCenterPoint();
9084
+ const curWaW = workarea.getScaledWidth();
9085
+ const curWaH = workarea.getScaledHeight();
9086
+ bgImg.set({
9087
+ left: center.x,
9088
+ top: center.y,
9089
+ originX: "center",
9090
+ originY: "center"
9091
+ });
9092
+ bgImg.setCoords();
9093
+ const exportLeft = Math.round(center.x - curWaW / 2);
9094
+ const exportTop = Math.round(center.y - curWaH / 2);
9095
+ const clip2 = createShapeExportClip(
9096
+ shapeType,
9097
+ { left: exportLeft, top: exportTop, width: curWaW, height: curWaH, rx, ry },
9098
+ currentSide
9099
+ );
9100
+ if (clip2) {
9101
+ clip2.absolutePositioned = true;
9102
+ bgImg.clipPath = clip2;
9103
+ }
9051
9104
  }
9052
9105
  createOverlays(canvas2, workarea, currentSide);
9053
9106
  canvas2.sendObjectToBack(workarea);
@@ -9066,8 +9119,7 @@
9066
9119
  } else {
9067
9120
  obj.set({
9068
9121
  selectable: false,
9069
- evented: false,
9070
- hasControls: false
9122
+ evented: false
9071
9123
  });
9072
9124
  }
9073
9125
  obj.setCoords();
@@ -9085,69 +9137,89 @@
9085
9137
  }, 40);
9086
9138
  });
9087
9139
  }
9088
- function getWorkareaDataUrl(cvs, multiplier = 1, pageSide) {
9140
+ function getWorkareaDataUrl(cvs, multiplier = 1, pageSide, includeGuidelines = false) {
9089
9141
  const wa2 = cvs.getObjects().find((o2) => o2.name === "workarea");
9090
9142
  cvs.discardActiveObject();
9091
- const guides = cvs.getObjects().filter(
9143
+ const guidesAndOverlays = cvs.getObjects().filter(
9092
9144
  (o2) => o2.name?.startsWith("guideline-") || o2.name?.startsWith("overlay-") || o2.name === "guideline-trim" || o2.name === "guideline-safe" || o2.excludeFromExport === true
9093
9145
  );
9094
- const prevVis = guides.map((g2) => g2.visible);
9095
- guides.forEach((g2) => {
9096
- g2.visible = false;
9146
+ const prevVis = guidesAndOverlays.map((g2) => g2.visible);
9147
+ guidesAndOverlays.forEach((g2) => {
9148
+ const isGuideline = g2.name?.startsWith("guideline-") || g2.name === "guideline-trim" || g2.name === "guideline-safe";
9149
+ if (includeGuidelines && isGuideline) {
9150
+ g2.visible = true;
9151
+ } else {
9152
+ g2.visible = false;
9153
+ }
9097
9154
  });
9155
+ if (includeGuidelines) {
9156
+ const activeGuides = guidesAndOverlays.filter(
9157
+ (g2) => g2.name?.startsWith("guideline-") || g2.name === "guideline-trim" || g2.name === "guideline-safe"
9158
+ );
9159
+ activeGuides.forEach((g2) => cvs.bringObjectToFront(g2));
9160
+ }
9098
9161
  const prevBg = cvs.backgroundColor;
9099
9162
  cvs.backgroundColor = "";
9163
+ const prevVt = cvs.viewportTransform ? [...cvs.viewportTransform] : [1, 0, 0, 1, 0, 0];
9164
+ const prevZoom = cvs.getZoom();
9165
+ cvs.setViewportTransform([1, 0, 0, 1, 0, 0]);
9166
+ cvs.setZoom(1);
9100
9167
  let dataUrl = "";
9101
- if (!wa2) {
9102
- cvs.renderAll();
9103
- dataUrl = cvs.toDataURL({ format: "png", multiplier });
9104
- } else {
9105
- const { left = 0, top = 0, scaleX = 1, scaleY = 1, width = 0, height = 0 } = wa2;
9106
- const curW = width * scaleX;
9107
- const curH = height * scaleY;
9108
- const shapeType = detectShape(wa2.__shapeType);
9109
- const activeSide = pageSide || wa2.__pageSide || useHistoryStore.getState().currentPage || "front";
9110
- const baseRx = (wa2.rx || wa2.__radiusOfCanvas || 0) * scaleX;
9111
- const baseRy = (wa2.ry || wa2.__radiusOfCanvas || 0) * scaleY;
9112
- let exportLeft = left;
9113
- let exportTop = top;
9114
- let exportW = curW;
9115
- let exportH = curH;
9116
- if (shapeType === "circle") {
9117
- const diam = Math.min(curW, curH);
9118
- exportLeft = left + (curW - diam) / 2;
9119
- exportTop = top + (curH - diam) / 2;
9120
- exportW = diam;
9121
- exportH = diam;
9122
- }
9123
- const clipObj = createShapeExportClip(
9124
- shapeType,
9125
- { left, top, width: curW, height: curH, rx: baseRx, ry: baseRy },
9126
- activeSide
9127
- );
9128
- const prevClip = cvs.clipPath;
9129
- if (clipObj) {
9130
- cvs.clipPath = clipObj;
9168
+ try {
9169
+ if (!wa2) {
9170
+ cvs.renderAll();
9171
+ dataUrl = cvs.toDataURL({ format: "png", multiplier });
9172
+ } else {
9173
+ const center = wa2.getCenterPoint();
9174
+ const scaledW = wa2.getScaledWidth();
9175
+ const scaledH = wa2.getScaledHeight();
9176
+ const shapeType = detectShape(wa2.__shapeType);
9177
+ const activeSide = pageSide || wa2.__pageSide || useHistoryStore.getState().currentPage || "front";
9178
+ const baseRx = (wa2.rx || wa2.__radiusOfCanvas || 0) * (wa2.scaleX || 1);
9179
+ const baseRy = (wa2.ry || wa2.__radiusOfCanvas || 0) * (wa2.scaleY || 1);
9180
+ let exportW = scaledW;
9181
+ let exportH = scaledH;
9182
+ let exportLeft = center.x - exportW / 2;
9183
+ let exportTop = center.y - exportH / 2;
9184
+ if (shapeType === "circle") {
9185
+ const diam = Math.min(scaledW, scaledH);
9186
+ exportW = diam;
9187
+ exportH = diam;
9188
+ exportLeft = center.x - diam / 2;
9189
+ exportTop = center.y - diam / 2;
9190
+ }
9191
+ const clipObj = createShapeExportClip(
9192
+ shapeType,
9193
+ { left: exportLeft, top: exportTop, width: exportW, height: exportH, rx: baseRx, ry: baseRy },
9194
+ activeSide
9195
+ );
9196
+ const prevClip = cvs.clipPath;
9197
+ if (clipObj) {
9198
+ cvs.clipPath = clipObj;
9199
+ }
9200
+ cvs.renderAll();
9201
+ dataUrl = cvs.toDataURL({
9202
+ format: "png",
9203
+ multiplier,
9204
+ left: exportLeft,
9205
+ top: exportTop,
9206
+ width: exportW,
9207
+ height: exportH
9208
+ });
9209
+ cvs.clipPath = prevClip;
9131
9210
  }
9132
- cvs.renderAll();
9133
- dataUrl = cvs.toDataURL({
9134
- format: "png",
9135
- multiplier,
9136
- left: exportLeft,
9137
- top: exportTop,
9138
- width: exportW,
9139
- height: exportH
9211
+ } finally {
9212
+ cvs.setViewportTransform(prevVt);
9213
+ cvs.setZoom(prevZoom);
9214
+ guidesAndOverlays.forEach((g2, i2) => {
9215
+ g2.visible = prevVis[i2];
9140
9216
  });
9141
- cvs.clipPath = prevClip;
9217
+ cvs.backgroundColor = prevBg;
9218
+ cvs.renderAll();
9142
9219
  }
9143
- guides.forEach((g2, i2) => {
9144
- g2.visible = prevVis[i2];
9145
- });
9146
- cvs.backgroundColor = prevBg;
9147
- cvs.renderAll();
9148
9220
  return dataUrl;
9149
9221
  }
9150
- async function renderPageDataUrl(json, baseCanvas, multiplier = 1, targetSide) {
9222
+ async function renderPageDataUrl(json, baseCanvas, multiplier = 1, targetSide, includeGuidelines = false) {
9151
9223
  if (!json) return "";
9152
9224
  try {
9153
9225
  const hiddenEl = document.createElement("canvas");
@@ -9165,7 +9237,7 @@
9165
9237
  hiddenCanvas.requestRenderAll();
9166
9238
  await new Promise((r2) => setTimeout(r2, 60));
9167
9239
  hiddenCanvas.renderAll();
9168
- const url = getWorkareaDataUrl(hiddenCanvas, multiplier, targetSide);
9240
+ const url = getWorkareaDataUrl(hiddenCanvas, multiplier, targetSide, includeGuidelines);
9169
9241
  hiddenCanvas.dispose();
9170
9242
  hiddenEl.remove();
9171
9243
  return url;
@@ -9174,6 +9246,28 @@
9174
9246
  return "";
9175
9247
  }
9176
9248
  }
9249
+ const HISTORY_CUSTOM_PROPERTIES = [
9250
+ "name",
9251
+ "id",
9252
+ "selectable",
9253
+ "evented",
9254
+ "hasControls",
9255
+ "lockMovementX",
9256
+ "lockMovementY",
9257
+ "rx",
9258
+ "ry",
9259
+ "__radiusOfCanvas",
9260
+ "__shapeType",
9261
+ "__pageSide",
9262
+ "__printWidth",
9263
+ "__printHeight",
9264
+ "__printDPI",
9265
+ "__baseScale",
9266
+ "__initialZoom",
9267
+ "__borderRect",
9268
+ "__rotationControlSize",
9269
+ "__showDegree"
9270
+ ];
9177
9271
  const useHistoryStore = create((set, get) => ({
9178
9272
  canvas: null,
9179
9273
  currentPage: "front",
@@ -9183,34 +9277,26 @@
9183
9277
  back: { history: [], redo: [] }
9184
9278
  },
9185
9279
  setCanvas: (canvas2) => {
9186
- set((state) => ({
9280
+ set({
9187
9281
  canvas: canvas2,
9282
+ isRestoring: false,
9188
9283
  pages: {
9189
9284
  front: { history: [], redo: [] },
9190
9285
  back: { history: [], redo: [] }
9191
9286
  }
9192
- }));
9287
+ });
9193
9288
  },
9194
- // setCanvas: (canvas) => {
9195
- // set((state) => ({
9196
- // canvas,
9197
- // pages: {
9198
- // ...state.pages,
9199
- // front: {
9200
- // history: [
9201
- // { objects: [] }
9202
- // ],
9203
- // redo: [],
9204
- // },
9205
- // },
9206
- // }));
9207
- // },
9208
9289
  setPage: (page) => set({ currentPage: page }),
9209
9290
  saveState: () => {
9210
9291
  const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9211
9292
  if (!canvas2 || isRestoring) return;
9212
- const json = canvas2.toObject(["name"]);
9213
- const history = pages[currentPage].history;
9293
+ const rawJson = canvas2.toObject(HISTORY_CUSTOM_PROPERTIES);
9294
+ const cleanObjects = (rawJson.objects || []).filter((obj) => {
9295
+ const name = obj.name || "";
9296
+ return name !== "watermark" && !name.startsWith("overlay-") && !name.startsWith("guideline-");
9297
+ });
9298
+ const json = { ...rawJson, objects: cleanObjects };
9299
+ const history = pages[currentPage]?.history ?? [];
9214
9300
  const last2 = history[history.length - 1];
9215
9301
  if (last2 && JSON.stringify(last2) === JSON.stringify(json)) {
9216
9302
  return;
@@ -9226,9 +9312,11 @@
9226
9312
  });
9227
9313
  },
9228
9314
  undo: async () => {
9229
- const { canvas: canvas2, currentPage, pages } = get();
9315
+ const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9316
+ if (!canvas2 || isRestoring) return;
9230
9317
  const page = pages[currentPage];
9231
- if (!canvas2 || page.history.length <= 1) return;
9318
+ if (!page || page.history.length <= 1) return;
9319
+ canvas2.discardActiveObject();
9232
9320
  const current = page.history.at(-1);
9233
9321
  const previous = page.history.at(-2);
9234
9322
  set({
@@ -9241,14 +9329,28 @@
9241
9329
  }
9242
9330
  }
9243
9331
  });
9244
- await loadPageToCanvas(canvas2, previous);
9245
- canvas2.requestRenderAll();
9246
- set({ isRestoring: false });
9332
+ try {
9333
+ await loadPageToCanvas(canvas2, previous, currentPage);
9334
+ canvas2.requestRenderAll();
9335
+ const editorStore = useEditorStore.getState();
9336
+ if (editorStore.pages) {
9337
+ editorStore.setPages({
9338
+ ...editorStore.pages,
9339
+ [currentPage]: previous
9340
+ });
9341
+ }
9342
+ } finally {
9343
+ setTimeout(() => {
9344
+ set({ isRestoring: false });
9345
+ }, 80);
9346
+ }
9247
9347
  },
9248
9348
  redoAction: async () => {
9249
- const { canvas: canvas2, currentPage, pages } = get();
9349
+ const { canvas: canvas2, currentPage, pages, isRestoring } = get();
9350
+ if (!canvas2 || isRestoring) return;
9250
9351
  const page = pages[currentPage];
9251
- if (!canvas2 || page.redo.length === 0) return;
9352
+ if (!page || page.redo.length === 0) return;
9353
+ canvas2.discardActiveObject();
9252
9354
  const next = page.redo[0];
9253
9355
  set({
9254
9356
  isRestoring: true,
@@ -9260,9 +9362,21 @@
9260
9362
  }
9261
9363
  }
9262
9364
  });
9263
- await loadPageToCanvas(canvas2, next);
9264
- canvas2.requestRenderAll();
9265
- set({ isRestoring: false });
9365
+ try {
9366
+ await loadPageToCanvas(canvas2, next, currentPage);
9367
+ canvas2.requestRenderAll();
9368
+ const editorStore = useEditorStore.getState();
9369
+ if (editorStore.pages) {
9370
+ editorStore.setPages({
9371
+ ...editorStore.pages,
9372
+ [currentPage]: next
9373
+ });
9374
+ }
9375
+ } finally {
9376
+ setTimeout(() => {
9377
+ set({ isRestoring: false });
9378
+ }, 80);
9379
+ }
9266
9380
  }
9267
9381
  }));
9268
9382
  const FontService = {
@@ -9597,6 +9711,189 @@
9597
9711
  });
9598
9712
  triggerDownload(URL.createObjectURL(blob), "design.json");
9599
9713
  }
9714
+ const ShapeService = {
9715
+ async getAll(data) {
9716
+ const res = await apiFetch(
9717
+ `/api/cliparts?page=${data.page}&limit=${data.limit}&category=${data.category}&status=true`,
9718
+ {
9719
+ method: "GET"
9720
+ }
9721
+ );
9722
+ return {
9723
+ items: res.data || []
9724
+ };
9725
+ },
9726
+ async getAllExternal(data) {
9727
+ const res = await apiFetch(
9728
+ `/api/external/images?page=${data.page}&limit=${data.limit}&keyType=${data.keyType}&status=true${data.query ? `&query=${data.query}` : ""}`,
9729
+ {
9730
+ method: "GET"
9731
+ }
9732
+ );
9733
+ return {
9734
+ items: res.data || []
9735
+ };
9736
+ },
9737
+ async getTextTemplates(data) {
9738
+ const res = await apiFetch(
9739
+ `/api/text-templates?page=${data.page}&limit=${data.limit}&status=true`,
9740
+ {
9741
+ method: "GET"
9742
+ }
9743
+ );
9744
+ return {
9745
+ items: res.data || []
9746
+ };
9747
+ },
9748
+ async getTextTemplateById(id) {
9749
+ const res = await apiFetch(
9750
+ `/api/text-templates/${id}`,
9751
+ {
9752
+ method: "GET"
9753
+ }
9754
+ );
9755
+ return res.data || null;
9756
+ },
9757
+ async updateTextTemplate(id, body) {
9758
+ const res = await apiFetch(`/api/text-templates/${id}`, {
9759
+ method: "PUT",
9760
+ body
9761
+ });
9762
+ return res;
9763
+ },
9764
+ async convertImages(url) {
9765
+ const res = await apiFetch(`/api/convert-images`, {
9766
+ method: "POST",
9767
+ body: { file: url }
9768
+ });
9769
+ return res;
9770
+ }
9771
+ };
9772
+ async function getBase64ImageFromURL(url) {
9773
+ if (!url) return url;
9774
+ if (url.startsWith("data:image/png") || url.startsWith("data:image/jpeg")) {
9775
+ return url;
9776
+ }
9777
+ const cleanUrl = url.replace(/&amp;/g, "&").trim();
9778
+ const fullUrl = cleanUrl.startsWith("http://") || cleanUrl.startsWith("https://") || cleanUrl.startsWith("blob:") || cleanUrl.startsWith("data:") ? cleanUrl : cleanUrl.startsWith("/") ? `https://upload.vibrantprint.com${cleanUrl}` : `https://upload.vibrantprint.com/${cleanUrl}`;
9779
+ if (typeof window === "undefined" || typeof document === "undefined") {
9780
+ return fullUrl;
9781
+ }
9782
+ try {
9783
+ const dataUrl = await new Promise((resolve, reject) => {
9784
+ const timer = setTimeout(() => reject(new Error("Image load timeout")), 4e3);
9785
+ const img = new Image();
9786
+ img.crossOrigin = "anonymous";
9787
+ img.onload = () => {
9788
+ clearTimeout(timer);
9789
+ try {
9790
+ const cvs = document.createElement("canvas");
9791
+ cvs.width = img.naturalWidth || img.width || 1;
9792
+ cvs.height = img.naturalHeight || img.height || 1;
9793
+ const ctx = cvs.getContext("2d");
9794
+ if (!ctx) return reject(new Error("No 2d context"));
9795
+ ctx.drawImage(img, 0, 0);
9796
+ resolve(cvs.toDataURL("image/png"));
9797
+ } catch (e2) {
9798
+ reject(e2);
9799
+ }
9800
+ };
9801
+ img.onerror = (e2) => {
9802
+ clearTimeout(timer);
9803
+ reject(e2);
9804
+ };
9805
+ img.src = fullUrl;
9806
+ });
9807
+ if (dataUrl && (dataUrl.startsWith("data:image/png") || dataUrl.startsWith("data:image/jpeg"))) {
9808
+ return dataUrl;
9809
+ }
9810
+ } catch {
9811
+ }
9812
+ try {
9813
+ const controller = new AbortController();
9814
+ const fetchTimer = setTimeout(() => controller.abort(), 4e3);
9815
+ const res = await fetch(fullUrl, { signal: controller.signal });
9816
+ clearTimeout(fetchTimer);
9817
+ const blob = await res.blob();
9818
+ const dataUrl = await new Promise((resolve, reject) => {
9819
+ const reader = new FileReader();
9820
+ reader.onloadend = () => {
9821
+ const result = reader.result;
9822
+ if (result && !result.startsWith("data:image/png") && !result.startsWith("data:image/jpeg")) {
9823
+ const img = new Image();
9824
+ const imgTimer = setTimeout(() => resolve(result), 3e3);
9825
+ img.onload = () => {
9826
+ clearTimeout(imgTimer);
9827
+ const cvs = document.createElement("canvas");
9828
+ cvs.width = img.naturalWidth || img.width || 1;
9829
+ cvs.height = img.naturalHeight || img.height || 1;
9830
+ const ctx = cvs.getContext("2d");
9831
+ if (ctx) {
9832
+ ctx.drawImage(img, 0, 0);
9833
+ resolve(cvs.toDataURL("image/png"));
9834
+ } else {
9835
+ resolve(result);
9836
+ }
9837
+ };
9838
+ img.onerror = () => {
9839
+ clearTimeout(imgTimer);
9840
+ resolve(result);
9841
+ };
9842
+ img.src = result;
9843
+ } else {
9844
+ resolve(result);
9845
+ }
9846
+ };
9847
+ reader.onerror = reject;
9848
+ reader.readAsDataURL(blob);
9849
+ });
9850
+ return dataUrl;
9851
+ } catch {
9852
+ try {
9853
+ const backendRes = await ShapeService.convertImages(fullUrl);
9854
+ const converted = backendRes?.url || backendRes?.dataUrl;
9855
+ if (converted && (converted.startsWith("data:image/png") || converted.startsWith("data:image/jpeg"))) {
9856
+ return converted;
9857
+ }
9858
+ } catch {
9859
+ }
9860
+ return fullUrl;
9861
+ }
9862
+ }
9863
+ async function addImageToCanvas(url) {
9864
+ const canvas2 = useEditorStore.getState().canvas;
9865
+ if (!canvas2) return;
9866
+ const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
9867
+ if (!workarea) return;
9868
+ let finalUrl = url;
9869
+ try {
9870
+ const b64 = await getBase64ImageFromURL(url);
9871
+ if (b64 && (b64.startsWith("data:image/png") || b64.startsWith("data:image/jpeg"))) {
9872
+ finalUrl = b64;
9873
+ }
9874
+ } catch {
9875
+ }
9876
+ oa.fromURL(finalUrl, { crossOrigin: "anonymous" }).then((img) => {
9877
+ const workareaCenter = workarea.getCenterPoint();
9878
+ const workareaBounds = workarea.getBoundingRect();
9879
+ const maxWidth = workareaBounds.width * 0.7;
9880
+ const maxHeight = workareaBounds.height * 0.7;
9881
+ img.scale(Math.min(maxWidth / img.width, maxHeight / img.height));
9882
+ img.set({
9883
+ left: workareaCenter.x,
9884
+ top: workareaCenter.y,
9885
+ originX: "center",
9886
+ originY: "center",
9887
+ selectable: true,
9888
+ hasControls: true
9889
+ });
9890
+ canvas2.add(img);
9891
+ canvas2.setActiveObject(img);
9892
+ canvas2.requestRenderAll();
9893
+ }).catch((err) => {
9894
+ console.warn("Fabric failed to load image from URL:", url, err);
9895
+ });
9896
+ }
9600
9897
  function captureWorkareaSVG(canvas2, workarea, pageSide) {
9601
9898
  const targetWorkarea = workarea || canvas2.getObjects().find((obj) => obj.name === "workarea");
9602
9899
  if (!targetWorkarea) return null;
@@ -9676,32 +9973,23 @@
9676
9973
  ];
9677
9974
  const urls = [
9678
9975
  ...new Set(
9679
- matches.map((m2) => m2[1]).filter((url) => url && (url.startsWith("http") || url.startsWith("blob:")))
9976
+ matches.map((m2) => m2[1]).filter((url) => url && (url.startsWith("http") || url.startsWith("blob:") || url.startsWith("data:") && !url.startsWith("data:image/png") && !url.startsWith("data:image/jpeg")))
9680
9977
  )
9681
9978
  ];
9682
9979
  if (urls.length === 0) return svg;
9683
9980
  await Promise.all(
9684
9981
  urls.map(async (url) => {
9685
- const base64 = await fetchImageAsBase64(url);
9686
- if (base64) result = result.split(`"${url}"`).join(`"${base64}"`);
9982
+ try {
9983
+ const base64 = await getBase64ImageFromURL(url);
9984
+ if (base64 && (base64.startsWith("data:image/png") || base64.startsWith("data:image/jpeg"))) {
9985
+ result = result.split(`"${url}"`).join(`"${base64}"`);
9986
+ }
9987
+ } catch {
9988
+ }
9687
9989
  })
9688
9990
  );
9689
9991
  return result;
9690
9992
  }
9691
- async function fetchImageAsBase64(url) {
9692
- try {
9693
- const res = await fetch(url, { mode: "cors" });
9694
- const blob = await res.blob();
9695
- return new Promise((resolve, reject) => {
9696
- const reader = new FileReader();
9697
- reader.onloadend = () => resolve(reader.result);
9698
- reader.onerror = reject;
9699
- reader.readAsDataURL(blob);
9700
- });
9701
- } catch {
9702
- return null;
9703
- }
9704
- }
9705
9993
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
9706
9994
  function getDefaultExportFromCjs(x2) {
9707
9995
  return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
@@ -99900,7 +100188,16 @@ end`);
99900
100188
  });
99901
100189
  };
99902
100190
  for (const [, obj] of context.enumerateIndirectObjects()) {
100191
+ const dict = obj.dict || (obj instanceof PDFDict ? obj : null);
100192
+ const subtype = dict?.get(PDFName.of("Subtype"))?.toString();
100193
+ const type = dict?.get(PDFName.of("Type"))?.toString();
100194
+ if (subtype === "/Image") {
100195
+ continue;
100196
+ }
99903
100197
  if (obj && obj.contents && obj.dict) {
100198
+ if (type === "/Font" || type === "/FontDescriptor" || type === "/Metadata") {
100199
+ continue;
100200
+ }
99904
100201
  let rawBytes = obj.contents;
99905
100202
  const filterStr = obj.dict.get(PDFName.of("Filter"))?.toString() || "";
99906
100203
  const isFlate = filterStr.includes("FlateDecode");
@@ -99922,7 +100219,7 @@ end`);
99922
100219
  } catch {
99923
100220
  }
99924
100221
  }
99925
- if (obj instanceof PDFDict) {
100222
+ if (obj instanceof PDFDict && subtype !== "/Image" && type !== "/Font") {
99926
100223
  for (const [k2, v2] of obj.entries()) {
99927
100224
  if (v2?.toString() === "/DeviceRGB") obj.set(k2, PDFName.of("DeviceCMYK"));
99928
100225
  }
@@ -99944,7 +100241,6 @@ end`);
99944
100241
  p2.node.set(PDFName.of("Resources"), res);
99945
100242
  }
99946
100243
  res.set(PDFName.of("ColorSpace"), context.obj({
99947
- DefaultRGB: PDFName.of("DeviceCMYK"),
99948
100244
  DefaultCMYK: PDFName.of("DeviceCMYK")
99949
100245
  }));
99950
100246
  }
@@ -100024,6 +100320,20 @@ end`);
100024
100320
  function sanitizeSvgForPdf(svg) {
100025
100321
  let result = svg;
100026
100322
  result = result.replace(/<\?xml[^>]*\?>/gi, "").replace(/<!DOCTYPE[^>]*>/gi, "").trim();
100323
+ if (!result.includes("xmlns:xlink=") && (result.includes("xlink:href") || result.includes("<image"))) {
100324
+ result = result.replace(/<svg\b([^>]*)>/i, '<svg$1 xmlns:xlink="http://www.w3.org/1999/xlink">');
100325
+ }
100326
+ result = result.replace(/<image\b([^>]*)>/gi, (match, attrs) => {
100327
+ let newAttrs = attrs;
100328
+ const xlinkMatch = attrs.match(/xlink:href="([^"]*)"/i);
100329
+ const hrefMatch = attrs.match(/\bhref="([^"]*)"/i);
100330
+ if (xlinkMatch && !hrefMatch) {
100331
+ newAttrs += ` href="${xlinkMatch[1]}"`;
100332
+ } else if (hrefMatch && !xlinkMatch) {
100333
+ newAttrs += ` xlink:href="${hrefMatch[1]}"`;
100334
+ }
100335
+ return `<image${newAttrs}>`;
100336
+ });
100027
100337
  result = result.replace(/font-family="([^"]+)"/g, (_2, p1) => {
100028
100338
  const first = p1.split(",")[0].replace(/&quot;/g, "").replace(/['"]/g, "").trim();
100029
100339
  return `font-family="${first}"`;
@@ -100070,17 +100380,114 @@ end`);
100070
100380
  }
100071
100381
  }
100072
100382
  if (rawBuffer && rawBuffer.byteLength > 0) {
100073
- const cmykBytes = await convertPdfToDeviceCMYK(rawBuffer);
100074
- const cmykBlob = new Blob([cmykBytes], { type: "application/pdf" });
100075
- triggerDownload(URL.createObjectURL(cmykBlob), filename);
100076
- } else {
100077
- doc.download(filename);
100383
+ try {
100384
+ const cmykBytes = await convertPdfToDeviceCMYK(rawBuffer);
100385
+ const cmykBlob = new Blob([cmykBytes], { type: "application/pdf" });
100386
+ triggerDownload(URL.createObjectURL(cmykBlob), filename);
100387
+ return;
100388
+ } catch (cmykErr) {
100389
+ console.warn("CMYK tagging failed, falling back to direct clean PDF buffer:", cmykErr);
100390
+ const fallbackBlob = new Blob([rawBuffer], { type: "application/pdf" });
100391
+ triggerDownload(URL.createObjectURL(fallbackBlob), filename);
100392
+ return;
100393
+ }
100394
+ }
100395
+ if (typeof doc.getBlob === "function") {
100396
+ const blob = await doc.getBlob();
100397
+ triggerDownload(URL.createObjectURL(blob), filename);
100398
+ return;
100078
100399
  }
100400
+ doc.download(filename);
100079
100401
  } catch (err) {
100080
- console.error("CMYK tagging error, falling back to direct download:", err);
100402
+ console.error("PDF generation/download error, trying fallback:", err);
100403
+ try {
100404
+ if (typeof doc.getBlob === "function") {
100405
+ const blob = await doc.getBlob();
100406
+ triggerDownload(URL.createObjectURL(blob), filename);
100407
+ return;
100408
+ }
100409
+ } catch {
100410
+ }
100081
100411
  doc.download(filename);
100082
100412
  }
100083
100413
  }
100414
+ function getAllCanvasImages(container) {
100415
+ const list = [];
100416
+ const objs = typeof container.getObjects === "function" ? container.getObjects() : [];
100417
+ for (const o2 of objs) {
100418
+ if (o2.type === "image" || o2.type === "fabricImage") {
100419
+ list.push(o2);
100420
+ } else if (o2.type === "group" && typeof o2.getObjects === "function") {
100421
+ list.push(...getAllCanvasImages(o2));
100422
+ }
100423
+ }
100424
+ return list;
100425
+ }
100426
+ async function inlineImageForExport(obj) {
100427
+ try {
100428
+ const el = obj._element || obj._originalElement;
100429
+ const currentSrc = el?.src || obj.src || (typeof obj.getSrc === "function" ? obj.getSrc() : "");
100430
+ if (currentSrc.startsWith("data:image/png") || currentSrc.startsWith("data:image/jpeg")) {
100431
+ return;
100432
+ }
100433
+ let base64 = null;
100434
+ if (el) {
100435
+ try {
100436
+ if (typeof el.toDataURL === "function") {
100437
+ base64 = el.toDataURL("image/png");
100438
+ } else {
100439
+ const w2 = el.naturalWidth || el.width;
100440
+ const h2 = el.naturalHeight || el.height;
100441
+ if (w2 > 0 && h2 > 0) {
100442
+ const cvs = document.createElement("canvas");
100443
+ cvs.width = w2;
100444
+ cvs.height = h2;
100445
+ const ctx = cvs.getContext("2d");
100446
+ if (ctx) {
100447
+ ctx.drawImage(el, 0, 0);
100448
+ base64 = cvs.toDataURL("image/png");
100449
+ }
100450
+ }
100451
+ }
100452
+ } catch {
100453
+ }
100454
+ }
100455
+ if (!base64 || !base64.startsWith("data:image/png") && !base64.startsWith("data:image/jpeg")) {
100456
+ if (currentSrc) {
100457
+ base64 = await getBase64ImageFromURL(currentSrc);
100458
+ }
100459
+ }
100460
+ if (base64 && (base64.startsWith("data:image/png") || base64.startsWith("data:image/jpeg"))) {
100461
+ if (obj._element) {
100462
+ obj._element.src = base64;
100463
+ if (typeof obj._element.setAttribute === "function") {
100464
+ try {
100465
+ obj._element.setAttribute("src", base64);
100466
+ } catch {
100467
+ }
100468
+ }
100469
+ if (typeof obj._element.decode === "function") {
100470
+ try {
100471
+ await obj._element.decode();
100472
+ } catch {
100473
+ }
100474
+ }
100475
+ }
100476
+ if (obj._originalElement) {
100477
+ obj._originalElement.src = base64;
100478
+ if (typeof obj._originalElement.setAttribute === "function") {
100479
+ try {
100480
+ obj._originalElement.setAttribute("src", base64);
100481
+ } catch {
100482
+ }
100483
+ }
100484
+ }
100485
+ obj.src = base64;
100486
+ }
100487
+ } catch (e2) {
100488
+ console.warn("Failed to inline image for export:", e2);
100489
+ }
100490
+ }
100084
100491
  async function downloadCanvasWorkarea(canvas2, type, scale2 = 1, pages, currentPageSide, dpi) {
100085
100492
  const workarea = canvas2.getObjects().find((obj) => obj.name === "workarea");
100086
100493
  if (!workarea) {
@@ -100091,6 +100498,10 @@ end`);
100091
100498
  exportDesignJSON(canvas2, workarea);
100092
100499
  return;
100093
100500
  }
100501
+ const activeImages = getAllCanvasImages(canvas2);
100502
+ for (const imgObj of activeImages) {
100503
+ await inlineImageForExport(imgObj);
100504
+ }
100094
100505
  const hiddenEl = document.createElement("canvas");
100095
100506
  hiddenEl.width = canvas2.width;
100096
100507
  hiddenEl.height = canvas2.height;
@@ -100112,6 +100523,8 @@ end`);
100112
100523
  "cropY",
100113
100524
  "rx",
100114
100525
  "ry",
100526
+ "crossOrigin",
100527
+ "src",
100115
100528
  "__radiusOfCanvas",
100116
100529
  "__shapeType",
100117
100530
  "__pageSide"
@@ -100121,8 +100534,31 @@ end`);
100121
100534
  const activeSide = pageSide || target?.__pageSide || currentSide;
100122
100535
  const curW = target.getScaledWidth ? target.getScaledWidth() : target.width * (target.scaleX || 1);
100123
100536
  const curH = target.getScaledHeight ? target.getScaledHeight() : target.height * (target.scaleY || 1);
100124
- const curLeft = target.left || 0;
100125
- const curTop = target.top || 0;
100537
+ const center = target.getCenterPoint ? target.getCenterPoint() : { x: (target.left || 0) + curW / 2, y: (target.top || 0) + curH / 2 };
100538
+ const bgImg = hiddenCanvas.getObjects().find((o2) => o2.name === "bg-image");
100539
+ if (bgImg) {
100540
+ bgImg.set({
100541
+ left: center.x,
100542
+ top: center.y,
100543
+ originX: "center",
100544
+ originY: "center"
100545
+ });
100546
+ bgImg.setCoords();
100547
+ const shapeType2 = detectShape(target.__shapeType);
100548
+ const baseRx2 = (target.rx || target.__radiusOfCanvas || 0) * (target.scaleX || 1);
100549
+ const baseRy2 = (target.ry || target.__radiusOfCanvas || 0) * (target.scaleY || 1);
100550
+ const exportLeft2 = Math.round(center.x - curW / 2);
100551
+ const exportTop2 = Math.round(center.y - curH / 2);
100552
+ const clip2 = createShapeExportClip(
100553
+ shapeType2,
100554
+ { left: exportLeft2, top: exportTop2, width: curW, height: curH, rx: baseRx2, ry: baseRy2 },
100555
+ activeSide
100556
+ );
100557
+ if (clip2) {
100558
+ clip2.absolutePositioned = true;
100559
+ bgImg.clipPath = clip2;
100560
+ }
100561
+ }
100126
100562
  if (format === "jpeg") hiddenCanvas.backgroundColor = "#ffffff";
100127
100563
  const wm = createWatermark(target, activeSide);
100128
100564
  hiddenCanvas.add(wm);
@@ -100136,35 +100572,40 @@ end`);
100136
100572
  const shapeType = detectShape(target.__shapeType);
100137
100573
  const baseRx = (target.rx || target.__radiusOfCanvas || 0) * (target.scaleX || 1);
100138
100574
  const baseRy = (target.ry || target.__radiusOfCanvas || 0) * (target.scaleY || 1);
100139
- let exportLeft = curLeft;
100140
- let exportTop = curTop;
100141
100575
  let exportW = curW;
100142
100576
  let exportH = curH;
100577
+ let exportLeft = center.x - exportW / 2;
100578
+ let exportTop = center.y - exportH / 2;
100143
100579
  if (shapeType === "circle") {
100144
100580
  const diam = Math.min(curW, curH);
100145
- exportLeft = curLeft + (curW - diam) / 2;
100146
- exportTop = curTop + (curH - diam) / 2;
100147
100581
  exportW = diam;
100148
100582
  exportH = diam;
100583
+ exportLeft = center.x - diam / 2;
100584
+ exportTop = center.y - diam / 2;
100149
100585
  }
100150
100586
  const exportClip = createShapeExportClip(
100151
100587
  shapeType,
100152
- { left: curLeft, top: curTop, width: curW, height: curH, rx: baseRx, ry: baseRy },
100588
+ { left: exportLeft, top: exportTop, width: exportW, height: exportH, rx: baseRx, ry: baseRy },
100153
100589
  activeSide
100154
100590
  );
100155
100591
  if (exportClip) {
100156
100592
  hiddenCanvas.clipPath = exportClip;
100157
100593
  }
100158
- const dataURL = hiddenCanvas.toDataURL({
100159
- format,
100160
- quality: 1,
100161
- multiplier: scale2,
100162
- left: exportLeft,
100163
- top: exportTop,
100164
- width: exportW,
100165
- height: exportH,
100166
- enableRetinaScaling: false
100167
- });
100594
+ let dataURL = "";
100595
+ try {
100596
+ dataURL = hiddenCanvas.toDataURL({
100597
+ format,
100598
+ quality: 1,
100599
+ multiplier: scale2,
100600
+ left: exportLeft,
100601
+ top: exportTop,
100602
+ width: exportW,
100603
+ height: exportH,
100604
+ enableRetinaScaling: false
100605
+ });
100606
+ } catch (taintErr) {
100607
+ console.warn("hiddenCanvas.toDataURL failed (tainted canvas):", taintErr);
100608
+ }
100168
100609
  hiddenCanvas.clipPath = void 0;
100169
100610
  hiddenCanvas.remove(wm);
100170
100611
  if (format === "jpeg") hiddenCanvas.backgroundColor = "";
@@ -100223,18 +100664,41 @@ end`);
100223
100664
  if (curWorkarea) {
100224
100665
  curWorkarea.__pageSide = page.side;
100225
100666
  }
100667
+ const canvasImages = getAllCanvasImages(hiddenCanvas);
100668
+ for (const imgObj of canvasImages) {
100669
+ await inlineImageForExport(imgObj);
100670
+ }
100671
+ hiddenCanvas.renderAll();
100226
100672
  const rawSvg = captureWithWatermark("svg", curWorkarea, page.side);
100227
100673
  if (rawSvg) {
100228
100674
  try {
100229
100675
  const processed = await processSVGWithEmbeds(rawSvg);
100230
100676
  const sanitized = sanitizeSvgForPdf(processed);
100231
- vectorPages.push({ svg: sanitized });
100677
+ const hasUnembedded = /<image\b[^>]*(?:href="https?:\/\/|href="blob:)[^>]*>/i.test(sanitized);
100678
+ if (hasUnembedded) {
100679
+ const rasterImage = captureWithWatermark("png", curWorkarea, page.side);
100680
+ if (rasterImage) {
100681
+ vectorPages.push({ image: rasterImage });
100682
+ } else {
100683
+ vectorPages.push({ svg: sanitized });
100684
+ }
100685
+ } else {
100686
+ vectorPages.push({ svg: sanitized });
100687
+ }
100232
100688
  } catch (err) {
100233
100689
  console.error("Vector SVG processing failed, falling back to image:", err);
100234
- vectorPages.push({ image: captureWithWatermark("png", curWorkarea, page.side) });
100690
+ const rasterImage = captureWithWatermark("png", curWorkarea, page.side);
100691
+ if (rasterImage) {
100692
+ vectorPages.push({ image: rasterImage });
100693
+ } else {
100694
+ vectorPages.push({ svg: rawSvg });
100695
+ }
100235
100696
  }
100236
100697
  } else {
100237
- vectorPages.push({ image: captureWithWatermark("png", curWorkarea, page.side) });
100698
+ const rasterImage = captureWithWatermark("png", curWorkarea, page.side);
100699
+ if (rasterImage) {
100700
+ vectorPages.push({ image: rasterImage });
100701
+ }
100238
100702
  }
100239
100703
  }
100240
100704
  await generateAndDownloadPDF(vectorPages, workarea.width, workarea.height);
@@ -100431,15 +100895,17 @@ end`);
100431
100895
  }
100432
100896
  );
100433
100897
  }
100434
- function PreviewModal({ onClose }) {
100898
+ function PreviewModal({ onClose, onUpload }) {
100435
100899
  const canvas2 = useEditorStore((s2) => s2.canvas);
100436
100900
  const { currentPage } = useHistoryStore();
100437
100901
  const productData = useProductStore((s2) => s2.productData);
100438
100902
  const isTwoSided = productData?.optionsMap?.printing === "4/4";
100439
100903
  const [viewMode, setViewMode] = React.useState("3d");
100904
+ const [showGuidelines, setShowGuidelines] = React.useState(true);
100440
100905
  const [activeSide, setActiveSide] = React.useState(currentPage || "front");
100441
- const [frontUrl, setFrontUrl] = React.useState("");
100442
- const [backUrl, setBackUrl] = React.useState("");
100906
+ const [frontSnapshot, setFrontSnapshot] = React.useState("");
100907
+ const [backSnapshot, setBackSnapshot] = React.useState("");
100908
+ const [isLoading, setIsLoading] = React.useState(true);
100443
100909
  const [rotX, setRotX] = React.useState(15);
100444
100910
  const [rotY, setRotY] = React.useState(-25);
100445
100911
  const [isAutoSpin, setIsAutoSpin] = React.useState(true);
@@ -100449,24 +100915,27 @@ end`);
100449
100915
  if (!canvas2) return;
100450
100916
  useHistoryStore.getState().saveState();
100451
100917
  let isMounted = true;
100918
+ setIsLoading(true);
100452
100919
  const loadSides = async () => {
100453
100920
  const currentSide = useHistoryStore.getState().currentPage || "front";
100454
100921
  const editorPages = useEditorStore.getState().pages || {};
100455
100922
  const historyPages = useHistoryStore.getState().pages || {};
100456
- const currentUrl = getWorkareaDataUrl(canvas2, 2, currentSide);
100923
+ const currentSnapshot = getWorkareaDataUrl(canvas2, 2, currentSide, showGuidelines);
100457
100924
  const opposingSide = currentSide === "front" ? "back" : "front";
100458
100925
  const opposingJson = editorPages[opposingSide] || historyPages[opposingSide]?.history?.at(-1) || null;
100459
- let opposingUrl = "";
100926
+ let opposingSnapshot = "";
100460
100927
  if (isTwoSided && opposingJson) {
100461
- opposingUrl = await renderPageDataUrl(opposingJson, canvas2, 2, opposingSide);
100928
+ opposingSnapshot = await renderPageDataUrl(opposingJson, canvas2, 2, opposingSide, showGuidelines);
100462
100929
  }
100930
+ const initialFront = currentSide === "front" ? currentSnapshot : opposingSnapshot;
100931
+ const initialBack = currentSide === "front" ? opposingSnapshot : currentSnapshot;
100463
100932
  if (isMounted) {
100464
- if (currentSide === "front") {
100465
- setFrontUrl(currentUrl);
100466
- setBackUrl(opposingUrl);
100467
- } else {
100468
- setBackUrl(currentUrl);
100469
- setFrontUrl(opposingUrl);
100933
+ setFrontSnapshot(initialFront);
100934
+ setBackSnapshot(initialBack);
100935
+ setIsLoading(false);
100936
+ if (onUpload) {
100937
+ const activeUrl = (activeSide === "front" ? initialFront : initialBack) || initialFront;
100938
+ onUpload({ front: initialFront, back: initialBack || void 0, active: activeUrl });
100470
100939
  }
100471
100940
  }
100472
100941
  };
@@ -100474,7 +100943,7 @@ end`);
100474
100943
  return () => {
100475
100944
  isMounted = false;
100476
100945
  };
100477
- }, [canvas2, isTwoSided]);
100946
+ }, [canvas2, isTwoSided, showGuidelines]);
100478
100947
  React.useEffect(() => {
100479
100948
  if (!isAutoSpin || isDragging || viewMode !== "3d") return;
100480
100949
  let animId;
@@ -100507,6 +100976,9 @@ end`);
100507
100976
  const isCircle = shapeType === "circle";
100508
100977
  const cardAspectRatio = isCircle ? 1 : waW && waH ? waW / waH : 1.75;
100509
100978
  const getShapeStyle = (side) => {
100979
+ if (showGuidelines) {
100980
+ return {};
100981
+ }
100510
100982
  const baseRadius = workarea?.rx || workarea?.__radiusOfCanvas || 0;
100511
100983
  const rxPct = waW > 0 ? (baseRadius || waW / 8) / waW * 100 : 12.5;
100512
100984
  const ryPct = waH > 0 ? (baseRadius || waH / 8) / waH * 100 : 12.5;
@@ -100548,14 +101020,14 @@ end`);
100548
101020
  return {};
100549
101021
  }
100550
101022
  };
100551
- const currentProof = activeSide === "front" ? frontUrl : backUrl;
101023
+ const currentProof = activeSide === "front" ? frontSnapshot : backSnapshot;
100552
101024
  const normalizedRotY = (rotY % 360 + 360) % 360;
100553
101025
  const isFrontFacing = normalizedRotY <= 90 || normalizedRotY >= 270;
100554
101026
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-[99999] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 select-none", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-6xl bg-white border border-gray-200 rounded-2xl shadow-2xl overflow-hidden flex flex-col h-[85vh]", children: [
100555
101027
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50/90", children: [
100556
101028
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
100557
101029
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-2.5 w-2.5 rounded-full bg-[#02bc71] animate-pulse" }),
100558
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-bold text-gray-900", children: "3D Design Preview" }),
101030
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-bold text-gray-900", children: "Design Preview" }),
100559
101031
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center bg-gray-200 p-0.5 rounded-lg border border-gray-300 text-xs", children: [
100560
101032
  /* @__PURE__ */ jsxRuntime.jsx(
100561
101033
  "button",
@@ -100575,7 +101047,23 @@ end`);
100575
101047
  children: "2D Proof"
100576
101048
  }
100577
101049
  )
100578
- ] })
101050
+ ] }),
101051
+ /* @__PURE__ */ jsxRuntime.jsxs(
101052
+ "button",
101053
+ {
101054
+ type: "button",
101055
+ onClick: () => setShowGuidelines((prev) => !prev),
101056
+ className: `flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-semibold cursor-pointer border transition-all ${showGuidelines ? "bg-emerald-50 text-emerald-800 border-emerald-300 shadow-sm" : "bg-white text-gray-600 border-gray-300 hover:bg-gray-100"}`,
101057
+ title: showGuidelines ? "Click to view finished trimmed cut" : "Click to view bleed, trim & safe line guidelines",
101058
+ children: [
101059
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `w-2 h-2 rounded-full ${showGuidelines ? "bg-emerald-500 animate-pulse" : "bg-gray-400"}` }),
101060
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
101061
+ "Guidelines: ",
101062
+ showGuidelines ? "ON" : "OFF"
101063
+ ] })
101064
+ ]
101065
+ }
101066
+ )
100579
101067
  ] }),
100580
101068
  /* @__PURE__ */ jsxRuntime.jsx(
100581
101069
  "button",
@@ -100587,6 +101075,27 @@ end`);
100587
101075
  }
100588
101076
  )
100589
101077
  ] }),
101078
+ showGuidelines && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between px-5 py-2 bg-emerald-50/70 border-b border-emerald-100 text-xs text-gray-700 gap-2", children: [
101079
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 flex-wrap", children: [
101080
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold text-emerald-950 flex items-center gap-1.5", children: [
101081
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-2 h-2 rounded-full bg-emerald-600" }),
101082
+ "Print Guidelines:"
101083
+ ] }),
101084
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
101085
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-4 h-0 border-t-2 border-dashed border-red-500 inline-block" }),
101086
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-red-700", children: "Trim Line (Cut line)" })
101087
+ ] }),
101088
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
101089
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-4 h-0 border-t-2 border-dashed border-emerald-600 inline-block" }),
101090
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-emerald-700", children: "Safe Line (Keep text inside)" })
101091
+ ] }),
101092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
101093
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3.5 h-3 rounded-sm border border-dashed border-gray-400 bg-white inline-block" }),
101094
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-600", children: "Bleed Area (Outer edge)" })
101095
+ ] })
101096
+ ] }),
101097
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-gray-500 italic hidden md:inline", children: "Toggle Guidelines OFF for finished cut preview" })
101098
+ ] }),
100590
101099
  /* @__PURE__ */ jsxRuntime.jsx(
100591
101100
  "div",
100592
101101
  {
@@ -100620,10 +101129,10 @@ end`);
100620
101129
  ...getShapeStyle("front")
100621
101130
  },
100622
101131
  className: "absolute inset-0 overflow-hidden bg-transparent",
100623
- children: frontUrl ? /* @__PURE__ */ jsxRuntime.jsx(
101132
+ children: frontSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
100624
101133
  "img",
100625
101134
  {
100626
- src: frontUrl,
101135
+ src: frontSnapshot,
100627
101136
  alt: "Front",
100628
101137
  style: getShapeStyle("front"),
100629
101138
  className: "w-full h-full object-contain pointer-events-none block"
@@ -100632,8 +101141,8 @@ end`);
100632
101141
  "div",
100633
101142
  {
100634
101143
  style: getShapeStyle("front"),
100635
- className: "w-full h-full bg-white flex items-center justify-center text-xs text-gray-400",
100636
- children: "Front Design"
101144
+ className: "w-full h-full bg-white flex flex-col items-center justify-center text-xs text-gray-500 gap-2 p-4 text-center",
101145
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Front Design" })
100637
101146
  }
100638
101147
  )
100639
101148
  }
@@ -100648,10 +101157,10 @@ end`);
100648
101157
  ...getShapeStyle("back")
100649
101158
  },
100650
101159
  className: "absolute inset-0 overflow-hidden bg-transparent",
100651
- children: isTwoSided ? backUrl ? /* @__PURE__ */ jsxRuntime.jsx(
101160
+ children: isTwoSided ? backSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
100652
101161
  "img",
100653
101162
  {
100654
- src: backUrl,
101163
+ src: backSnapshot,
100655
101164
  alt: "Back",
100656
101165
  style: getShapeStyle("back"),
100657
101166
  className: "w-full h-full object-contain pointer-events-none block"
@@ -100660,7 +101169,8 @@ end`);
100660
101169
  "div",
100661
101170
  {
100662
101171
  style: getShapeStyle("back"),
100663
- className: "w-full h-full bg-white flex flex-col items-center justify-center text-xs text-gray-400 p-4 text-center"
101172
+ className: "w-full h-full bg-white flex flex-col items-center justify-center text-xs text-gray-500 gap-2 p-4 text-center",
101173
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Back Design" })
100664
101174
  }
100665
101175
  ) : /* @__PURE__ */ jsxRuntime.jsx(
100666
101176
  "div",
@@ -100711,7 +101221,7 @@ end`);
100711
101221
  maxWidth: "85%",
100712
101222
  ...getShapeStyle(activeSide)
100713
101223
  },
100714
- className: "relative overflow-hidden bg-transparent flex items-center justify-center",
101224
+ className: `relative overflow-hidden flex items-center justify-center transition-all ${showGuidelines ? "bg-white shadow-xl ring-1 ring-black/10" : "bg-transparent shadow-lg"}`,
100715
101225
  children: /* @__PURE__ */ jsxRuntime.jsx(
100716
101226
  "img",
100717
101227
  {
@@ -100722,7 +101232,10 @@ end`);
100722
101232
  }
100723
101233
  )
100724
101234
  }
100725
- ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-gray-50 rounded-xl border border-gray-200 text-xs text-gray-400", children: [
101235
+ ) : isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-white rounded-2xl border border-gray-200 shadow-sm flex flex-col items-center justify-center gap-3", children: [
101236
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 border-3 border-[#02bc71] border-t-transparent rounded-full animate-spin" }),
101237
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-gray-800", children: "Generating preview..." })
101238
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-gray-50 rounded-xl border border-gray-200 text-xs text-gray-400", children: [
100726
101239
  "No preview available for ",
100727
101240
  activeSide,
100728
101241
  " side."
@@ -100780,7 +101293,10 @@ end`);
100780
101293
  children: "Reset"
100781
101294
  }
100782
101295
  )
100783
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 font-medium", children: "Flat 2D High-Resolution Render" }),
101296
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 text-xs", children: [
101297
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500 font-medium", children: "Flat 2D High-Resolution Render" }),
101298
+ showGuidelines ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800 font-semibold text-[11px] border border-emerald-300", children: "Bleed, Trim & Safe Lines" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-0.5 rounded-full bg-gray-200 text-gray-700 font-semibold text-[11px]", children: "Finished Trimmed Cut" })
101299
+ ] }),
100784
101300
  /* @__PURE__ */ jsxRuntime.jsx(
100785
101301
  "button",
100786
101302
  {
@@ -101473,64 +101989,6 @@ end`);
101473
101989
  }
101474
101990
  window.history.replaceState({}, "", url.toString());
101475
101991
  }
101476
- const ShapeService = {
101477
- async getAll(data) {
101478
- const res = await apiFetch(
101479
- `/api/cliparts?page=${data.page}&limit=${data.limit}&category=${data.category}&status=true`,
101480
- {
101481
- method: "GET"
101482
- }
101483
- );
101484
- return {
101485
- items: res.data || []
101486
- };
101487
- },
101488
- async getAllExternal(data) {
101489
- const res = await apiFetch(
101490
- `/api/external/images?page=${data.page}&limit=${data.limit}&keyType=${data.keyType}&status=true${data.query ? `&query=${data.query}` : ""}`,
101491
- {
101492
- method: "GET"
101493
- }
101494
- );
101495
- return {
101496
- items: res.data || []
101497
- };
101498
- },
101499
- async getTextTemplates(data) {
101500
- const res = await apiFetch(
101501
- `/api/text-templates?page=${data.page}&limit=${data.limit}&status=true`,
101502
- {
101503
- method: "GET"
101504
- }
101505
- );
101506
- return {
101507
- items: res.data || []
101508
- };
101509
- },
101510
- async getTextTemplateById(id) {
101511
- const res = await apiFetch(
101512
- `/api/text-templates/${id}`,
101513
- {
101514
- method: "GET"
101515
- }
101516
- );
101517
- return res.data || null;
101518
- },
101519
- async updateTextTemplate(id, body) {
101520
- const res = await apiFetch(`/api/text-templates/${id}`, {
101521
- method: "PUT",
101522
- body
101523
- });
101524
- return res;
101525
- },
101526
- async convertImages(url) {
101527
- const res = await apiFetch(`/api/convert-images`, {
101528
- method: "POST",
101529
- body: { file: url }
101530
- });
101531
- return res;
101532
- }
101533
- };
101534
101992
  function spaNavigate(path) {
101535
101993
  if (typeof window === "undefined") return;
101536
101994
  try {
@@ -102437,32 +102895,6 @@ end`);
102437
102895
  ]
102438
102896
  }
102439
102897
  ];
102440
- async function addImageToCanvas(url) {
102441
- const canvas2 = useEditorStore.getState().canvas;
102442
- if (!canvas2) return;
102443
- const workarea = canvas2.getObjects().find((o2) => o2.name === "workarea");
102444
- if (!workarea) return;
102445
- oa.fromURL(url, { crossOrigin: "anonymous" }).then((img) => {
102446
- const workareaCenter = workarea.getCenterPoint();
102447
- const workareaBounds = workarea.getBoundingRect();
102448
- const maxWidth = workareaBounds.width * 0.7;
102449
- const maxHeight = workareaBounds.height * 0.7;
102450
- img.scale(Math.min(maxWidth / img.width, maxHeight / img.height));
102451
- img.set({
102452
- left: workareaCenter.x,
102453
- top: workareaCenter.y,
102454
- originX: "center",
102455
- originY: "center",
102456
- selectable: true,
102457
- hasControls: true
102458
- });
102459
- canvas2.add(img);
102460
- canvas2.setActiveObject(img);
102461
- canvas2.requestRenderAll();
102462
- }).catch((err) => {
102463
- console.warn("Fabric failed to load image from URL:", url, err);
102464
- });
102465
- }
102466
102898
  function getWorkareaMetrics(canvas2) {
102467
102899
  const workarea = canvas2.getObjects().find((obj) => obj.name === "workarea");
102468
102900
  if (!workarea) return null;
@@ -103489,10 +103921,15 @@ end`);
103489
103921
  try {
103490
103922
  startLoading();
103491
103923
  const image = await ShapeService.convertImages(url);
103492
- const imageUrl = image?.success ? image?.url : null;
103493
- if (imageUrl) addImageToCanvas(imageUrl);
103924
+ const imageUrl = image?.success || image?.status ? image?.url : null;
103925
+ if (imageUrl) {
103926
+ addImageToCanvas(imageUrl);
103927
+ } else {
103928
+ alert(image?.data || image?.message || "Could not load image. Provider rate limit reached (429).");
103929
+ }
103494
103930
  } catch (error2) {
103495
103931
  console.error("Failed to convert image:", error2);
103932
+ alert(error2?.message || "Failed to convert image");
103496
103933
  } finally {
103497
103934
  stopLoading();
103498
103935
  if (window.innerWidth < 768) {
@@ -103737,14 +104174,24 @@ end`);
103737
104174
  startLoading();
103738
104175
  const targetUrl = item.largeImage || item.image;
103739
104176
  if (!targetUrl) return;
103740
- const image = await ShapeService.convertImages(targetUrl);
103741
- const imageUrl = image?.success ? image?.url : targetUrl;
103742
- if (imageUrl) addImageToCanvas(imageUrl);
103743
- if (window.innerWidth < 768) {
103744
- useSidePanelStore.getState().setActivePanel(null);
104177
+ let image = await ShapeService.convertImages(targetUrl);
104178
+ let imageUrl = image?.success || image?.status ? image?.url : null;
104179
+ if (!imageUrl && item.image && item.image !== targetUrl) {
104180
+ const fallbackRes = await ShapeService.convertImages(item.image);
104181
+ imageUrl = fallbackRes?.success || fallbackRes?.status ? fallbackRes?.url : item.image;
104182
+ }
104183
+ if (imageUrl) {
104184
+ await addImageToCanvas(imageUrl);
104185
+ if (window.innerWidth < 768) {
104186
+ useSidePanelStore.getState().setActivePanel(null);
104187
+ }
104188
+ } else {
104189
+ const errMsg = image?.data || image?.message || "Stock image provider rate limit reached (429). Please try another image.";
104190
+ alert(`Unable to load image: ${errMsg}`);
103745
104191
  }
103746
104192
  } catch (error2) {
103747
104193
  console.error("Failed to convert image:", error2);
104194
+ alert(`Image loading error: ${error2?.message || "Please try another image."}`);
103748
104195
  } finally {
103749
104196
  stopLoading();
103750
104197
  }
@@ -103774,17 +104221,21 @@ end`);
103774
104221
  canvas2.getObjects("image").forEach((o2) => {
103775
104222
  if (o2.name === "bg-image") canvas2.remove(o2);
103776
104223
  });
103777
- const bounds = workarea.getBoundingRect();
103778
- const centerX = bounds.left + bounds.width / 2;
103779
- const centerY = bounds.top + bounds.height / 2;
104224
+ const center = workarea.getCenterPoint();
104225
+ const waW = workarea.getScaledWidth();
104226
+ const waH = workarea.getScaledHeight();
104227
+ const shapeType = detectShape(workarea.__shapeType);
104228
+ const activeSide = workarea.__pageSide || useHistoryStore.getState().currentPage || "front";
104229
+ const baseRx = (workarea.rx || workarea.__radiusOfCanvas || 0) * (workarea.scaleX || 1);
104230
+ const baseRy = (workarea.ry || workarea.__radiusOfCanvas || 0) * (workarea.scaleY || 1);
103780
104231
  oa.fromURL(url + "?t=vb", { crossOrigin: "anonymous" }).then((img) => {
103781
104232
  const imgW = img.width || 1;
103782
104233
  const imgH = img.height || 1;
103783
- const scale2 = Math.max(bounds.width / imgW, bounds.height / imgH);
104234
+ const scale2 = Math.max(waW / imgW, waH / imgH);
103784
104235
  img.scale(scale2);
103785
104236
  img.set({
103786
- left: centerX,
103787
- top: centerY,
104237
+ left: center.x,
104238
+ top: center.y,
103788
104239
  originX: "center",
103789
104240
  originY: "center",
103790
104241
  selectable: false,
@@ -103794,15 +104245,22 @@ end`);
103794
104245
  lockMovementY: false,
103795
104246
  name: "bg-image"
103796
104247
  });
103797
- img.clipPath = new jr({
103798
- left: centerX,
103799
- top: centerY,
103800
- width: bounds.width,
103801
- height: bounds.height,
104248
+ const exportLeft = Math.round(center.x - waW / 2);
104249
+ const exportTop = Math.round(center.y - waH / 2);
104250
+ const clip2 = createShapeExportClip(
104251
+ shapeType,
104252
+ { left: exportLeft, top: exportTop, width: waW, height: waH, rx: baseRx, ry: baseRy },
104253
+ activeSide
104254
+ ) || new jr({
104255
+ left: center.x,
104256
+ top: center.y,
104257
+ width: waW,
104258
+ height: waH,
103802
104259
  originX: "center",
103803
104260
  originY: "center",
103804
104261
  absolutePositioned: true
103805
104262
  });
104263
+ img.clipPath = clip2;
103806
104264
  canvas2.add(img);
103807
104265
  canvas2.sendObjectToBack(img);
103808
104266
  canvas2.sendObjectToBack(workarea);
@@ -109453,6 +109911,282 @@ END:VCARD`;
109453
109911
  ] })
109454
109912
  ] });
109455
109913
  }
109914
+ function BgRemoveIcon({ size = 16, className = "" }) {
109915
+ return /* @__PURE__ */ jsxRuntime.jsxs(
109916
+ "svg",
109917
+ {
109918
+ width: size,
109919
+ height: size,
109920
+ viewBox: "0 0 24 24",
109921
+ fill: "none",
109922
+ stroke: "currentColor",
109923
+ strokeWidth: "2",
109924
+ strokeLinecap: "round",
109925
+ strokeLinejoin: "round",
109926
+ className: `shrink-0 ${className}`,
109927
+ children: [
109928
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3v3" }),
109929
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18.5 5.5l-2.1 2.1" }),
109930
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12h-3" }),
109931
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21l9-9" }),
109932
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.2 7.8a2 2 0 0 1 2.8 2.8L6.5 19.1a2 2 0 0 1-1.4.6H3v-2.1a2 2 0 0 1 .6-1.4l8.6-8.4z" })
109933
+ ]
109934
+ }
109935
+ );
109936
+ }
109937
+ const IMGLY_CDN_PATH = "https://staticimgly.com/@imgly/background-removal-data/1.7.0/dist/";
109938
+ let removeBackgroundFn = null;
109939
+ async function getRemoveBackground() {
109940
+ if (!removeBackgroundFn) {
109941
+ const mod = await import("@imgly/background-removal");
109942
+ removeBackgroundFn = mod.removeBackground || mod.default;
109943
+ }
109944
+ return removeBackgroundFn;
109945
+ }
109946
+ function blobToDataUrl(blob) {
109947
+ return new Promise((resolve, reject) => {
109948
+ const reader = new FileReader();
109949
+ reader.onloadend = () => resolve(reader.result);
109950
+ reader.onerror = reject;
109951
+ reader.readAsDataURL(blob);
109952
+ });
109953
+ }
109954
+ function loadImageElement(src) {
109955
+ return new Promise((resolve, reject) => {
109956
+ const img = new Image();
109957
+ img.crossOrigin = "anonymous";
109958
+ img.onload = () => resolve(img);
109959
+ img.onerror = (err) => reject(err);
109960
+ img.src = src;
109961
+ });
109962
+ }
109963
+ async function extractImageBlob(imageObj) {
109964
+ const currentElement = imageObj.getElement();
109965
+ const currentSrc = imageObj.getSrc() || currentElement?.src;
109966
+ if (currentSrc && (currentSrc.startsWith("data:") || currentSrc.startsWith("blob:"))) {
109967
+ try {
109968
+ const res = await fetch(currentSrc);
109969
+ const blob = await res.blob();
109970
+ if (blob && blob.size > 0) return blob;
109971
+ } catch (e2) {
109972
+ console.warn("Direct data/blob URL fetch failed, trying canvas export:", e2);
109973
+ }
109974
+ }
109975
+ if (currentElement && (currentElement.naturalWidth || currentElement.width)) {
109976
+ try {
109977
+ const w2 = currentElement.naturalWidth || currentElement.width;
109978
+ const h2 = currentElement.naturalHeight || currentElement.height;
109979
+ const offscreen = document.createElement("canvas");
109980
+ offscreen.width = w2;
109981
+ offscreen.height = h2;
109982
+ const ctx = offscreen.getContext("2d");
109983
+ if (ctx) {
109984
+ ctx.drawImage(currentElement, 0, 0, w2, h2);
109985
+ const blob = await new Promise((resolve) => {
109986
+ offscreen.toBlob((b2) => resolve(b2), "image/png");
109987
+ });
109988
+ if (blob && blob.size > 0) return blob;
109989
+ }
109990
+ } catch (e2) {
109991
+ console.warn("Offscreen canvas render failed (possible CORS):", e2);
109992
+ }
109993
+ }
109994
+ try {
109995
+ const dataUrl = imageObj.toDataURL({ format: "png" });
109996
+ if (dataUrl && dataUrl.startsWith("data:")) {
109997
+ const res = await fetch(dataUrl);
109998
+ const blob = await res.blob();
109999
+ if (blob && blob.size > 0) return blob;
110000
+ }
110001
+ } catch (e2) {
110002
+ console.warn("Fabric toDataURL failed:", e2);
110003
+ }
110004
+ if (currentSrc) {
110005
+ try {
110006
+ const res = await fetch(currentSrc, { mode: "cors" });
110007
+ const blob = await res.blob();
110008
+ if (blob && blob.size > 0) return blob;
110009
+ } catch (e2) {
110010
+ console.warn("Remote URL fetch failed:", e2);
110011
+ }
110012
+ }
110013
+ throw new Error("Unable to extract valid image blob from object.");
110014
+ }
110015
+ class BackgroundRemovalService {
110016
+ /**
110017
+ * Automatically cuts out the background of a FabricImage using client-side AI
110018
+ */
110019
+ static async removeBackgroundAI(canvas2, imageObj, onProgress) {
110020
+ if (!canvas2 || !imageObj) return false;
110021
+ const loader = useLoaderStore.getState();
110022
+ try {
110023
+ loader.startLoading("Extracting image data...");
110024
+ onProgress?.("Extracting image data...");
110025
+ const updateProgress = (msg) => {
110026
+ loader.setLoading(true, msg);
110027
+ onProgress?.(msg);
110028
+ };
110029
+ const currentElement = imageObj.getElement();
110030
+ const currentSrc = imageObj.getSrc() || currentElement?.src;
110031
+ if (!currentSrc && !currentElement) {
110032
+ throw new Error("Unable to locate image source");
110033
+ }
110034
+ if (!imageObj.__originalSrc) {
110035
+ imageObj.__originalSrc = currentSrc;
110036
+ }
110037
+ const imageBlob = await extractImageBlob(imageObj);
110038
+ updateProgress("Initializing AI model...");
110039
+ const removeBackground = await getRemoveBackground();
110040
+ let blob;
110041
+ try {
110042
+ blob = await removeBackground(imageBlob, {
110043
+ publicPath: IMGLY_CDN_PATH,
110044
+ model: "isnet_quint8",
110045
+ output: {
110046
+ format: "image/png",
110047
+ quality: 0.95
110048
+ },
110049
+ progress: (key, current, total) => {
110050
+ if (total > 0) {
110051
+ const pct = Math.round(current / total * 100);
110052
+ updateProgress(`Downloading AI model: ${pct}%`);
110053
+ } else {
110054
+ updateProgress("Processing AI model...");
110055
+ }
110056
+ }
110057
+ });
110058
+ } catch (err1) {
110059
+ console.warn("isnet_quint8 failed, attempting isnet_fp16 fallback:", err1);
110060
+ blob = await removeBackground(imageBlob, {
110061
+ publicPath: IMGLY_CDN_PATH,
110062
+ model: "isnet_fp16",
110063
+ output: {
110064
+ format: "image/png",
110065
+ quality: 0.95
110066
+ },
110067
+ progress: (key, current, total) => {
110068
+ if (total > 0) {
110069
+ const pct = Math.round(current / total * 100);
110070
+ updateProgress(`Downloading AI model: ${pct}%`);
110071
+ } else {
110072
+ updateProgress("Processing AI model...");
110073
+ }
110074
+ }
110075
+ });
110076
+ }
110077
+ updateProgress("Applying transparent cutout...");
110078
+ const transparentDataUrl = await blobToDataUrl(blob);
110079
+ const newImgElement = await loadImageElement(transparentDataUrl);
110080
+ const origScaleX = imageObj.scaleX ?? 1;
110081
+ const origScaleY = imageObj.scaleY ?? 1;
110082
+ const origWidth = imageObj.width ?? 1;
110083
+ const origHeight = imageObj.height ?? 1;
110084
+ imageObj.setElement(newImgElement);
110085
+ if (newImgElement.naturalWidth && newImgElement.naturalHeight) {
110086
+ imageObj.set({
110087
+ scaleX: origWidth * origScaleX / newImgElement.naturalWidth,
110088
+ scaleY: origHeight * origScaleY / newImgElement.naturalHeight
110089
+ });
110090
+ }
110091
+ imageObj.__isBgRemoved = true;
110092
+ imageObj.setCoords();
110093
+ canvas2.requestRenderAll();
110094
+ useHistoryStore.getState().saveState();
110095
+ return true;
110096
+ } catch (err) {
110097
+ console.error("Background removal error:", err);
110098
+ throw err;
110099
+ } finally {
110100
+ loader.stopLoading();
110101
+ }
110102
+ }
110103
+ /**
110104
+ * Restores the original image background prior to AI cutout
110105
+ */
110106
+ static async restoreOriginalBackground(canvas2, imageObj) {
110107
+ if (!canvas2 || !imageObj) return false;
110108
+ const originalSrc = imageObj.__originalSrc;
110109
+ if (!originalSrc) return false;
110110
+ const loader = useLoaderStore.getState();
110111
+ try {
110112
+ loader.startLoading("Restoring original background...");
110113
+ const origImgElement = await loadImageElement(originalSrc);
110114
+ const origScaleX = imageObj.scaleX ?? 1;
110115
+ const origScaleY = imageObj.scaleY ?? 1;
110116
+ const origWidth = imageObj.width ?? 1;
110117
+ const origHeight = imageObj.height ?? 1;
110118
+ imageObj.setElement(origImgElement);
110119
+ if (origImgElement.naturalWidth && origImgElement.naturalHeight) {
110120
+ imageObj.set({
110121
+ scaleX: origWidth * origScaleX / origImgElement.naturalWidth,
110122
+ scaleY: origHeight * origScaleY / origImgElement.naturalHeight
110123
+ });
110124
+ }
110125
+ delete imageObj.__originalSrc;
110126
+ imageObj.__isBgRemoved = false;
110127
+ this.removeColorRemovalFilter(canvas2, imageObj, false);
110128
+ imageObj.setCoords();
110129
+ canvas2.requestRenderAll();
110130
+ useHistoryStore.getState().saveState();
110131
+ return true;
110132
+ } catch (err) {
110133
+ console.error("Failed to restore original background:", err);
110134
+ return false;
110135
+ } finally {
110136
+ loader.stopLoading();
110137
+ }
110138
+ }
110139
+ /**
110140
+ * Applies Fabric native RemoveColor filter for instant solid-color transparency
110141
+ */
110142
+ static applyColorRemovalFilter(canvas2, imageObj, color = "#FFFFFF", distance = 0.15, saveHistory = true) {
110143
+ if (!canvas2 || !imageObj) return;
110144
+ const normDistance = Math.max(0.01, Math.min(0.99, distance));
110145
+ let filter = (imageObj.filters || []).find(
110146
+ (f) => f.type === "RemoveColor"
110147
+ );
110148
+ if (!filter) {
110149
+ filter = new Sh.RemoveColor({
110150
+ color,
110151
+ distance: normDistance
110152
+ });
110153
+ imageObj.filters = [...imageObj.filters || [], filter];
110154
+ } else {
110155
+ filter.color = color;
110156
+ filter.distance = normDistance;
110157
+ }
110158
+ imageObj.applyFilters();
110159
+ canvas2.requestRenderAll();
110160
+ if (saveHistory) {
110161
+ useHistoryStore.getState().saveState();
110162
+ }
110163
+ }
110164
+ /**
110165
+ * Removes the solid-color background filter
110166
+ */
110167
+ static removeColorRemovalFilter(canvas2, imageObj, saveHistory = true) {
110168
+ if (!canvas2 || !imageObj || !imageObj.filters) return;
110169
+ imageObj.filters = imageObj.filters.filter(
110170
+ (f) => f.type !== "RemoveColor"
110171
+ );
110172
+ imageObj.applyFilters();
110173
+ canvas2.requestRenderAll();
110174
+ if (saveHistory) {
110175
+ useHistoryStore.getState().saveState();
110176
+ }
110177
+ }
110178
+ /**
110179
+ * Checks if an image has active background removal (AI or color filter)
110180
+ */
110181
+ static hasBackgroundRemoval(imageObj) {
110182
+ if (!imageObj) return false;
110183
+ const hasAI = !!imageObj.__originalSrc || !!imageObj.__isBgRemoved;
110184
+ const hasFilter = !!(imageObj.filters || []).some(
110185
+ (f) => f.type === "RemoveColor"
110186
+ );
110187
+ return hasAI || hasFilter;
110188
+ }
110189
+ }
109456
110190
  function EffectsPanel() {
109457
110191
  const canvas2 = useEditorStore((s2) => s2.canvas);
109458
110192
  const saveState = useHistoryStore((s2) => s2.saveState);
@@ -109474,6 +110208,84 @@ END:VCARD`;
109474
110208
  const { objects } = useSelectionStore();
109475
110209
  const replaceState = useEffectStore((s2) => s2.replaceState);
109476
110210
  const activeObject = objects[0];
110211
+ const [isRemovingBg, setIsRemovingBg] = React.useState(false);
110212
+ const [bgProgress, setBgProgress] = React.useState("");
110213
+ const [colorRemovalEnabled, setColorRemovalEnabled] = React.useState(false);
110214
+ const [colorRemovalColor, setColorRemovalColor] = React.useState("#FFFFFF");
110215
+ const [colorRemovalDistance, setColorRemovalDistance] = React.useState(15);
110216
+ const isImg = !!activeObject && (activeObject.type === "image" || activeObject.type === "FabricImage" || activeObject.isType?.("image") || activeObject.isType?.("FabricImage") || typeof activeObject.getElement === "function");
110217
+ const hasOriginal = !!activeObject?.__originalSrc;
110218
+ const handleRemoveBgAI = async () => {
110219
+ if (!canvas2 || !activeObject || isRemovingBg) return;
110220
+ try {
110221
+ setIsRemovingBg(true);
110222
+ setBgProgress("Starting AI cutout...");
110223
+ await BackgroundRemovalService.removeBackgroundAI(
110224
+ canvas2,
110225
+ activeObject,
110226
+ (msg) => setBgProgress(msg)
110227
+ );
110228
+ } catch (err) {
110229
+ console.error("Failed to remove background:", err);
110230
+ alert("Could not remove background. Please ensure the image is accessible.");
110231
+ } finally {
110232
+ setIsRemovingBg(false);
110233
+ setBgProgress("");
110234
+ }
110235
+ };
110236
+ const handleRestoreBg = async () => {
110237
+ if (!canvas2 || !activeObject || isRemovingBg) return;
110238
+ await BackgroundRemovalService.restoreOriginalBackground(canvas2, activeObject);
110239
+ setColorRemovalEnabled(false);
110240
+ };
110241
+ const handleToggleColorRemoval = (enabled) => {
110242
+ if (!canvas2 || !activeObject) return;
110243
+ setColorRemovalEnabled(enabled);
110244
+ if (enabled) {
110245
+ BackgroundRemovalService.applyColorRemovalFilter(
110246
+ canvas2,
110247
+ activeObject,
110248
+ colorRemovalColor,
110249
+ colorRemovalDistance / 100,
110250
+ true
110251
+ );
110252
+ } else {
110253
+ BackgroundRemovalService.removeColorRemovalFilter(
110254
+ canvas2,
110255
+ activeObject,
110256
+ true
110257
+ );
110258
+ }
110259
+ };
110260
+ const handleColorChange = (hex) => {
110261
+ if (!canvas2 || !activeObject) return;
110262
+ setColorRemovalColor(hex);
110263
+ if (colorRemovalEnabled) {
110264
+ BackgroundRemovalService.applyColorRemovalFilter(
110265
+ canvas2,
110266
+ activeObject,
110267
+ hex,
110268
+ colorRemovalDistance / 100,
110269
+ true
110270
+ );
110271
+ }
110272
+ };
110273
+ const handleDistanceChange = (distPct) => {
110274
+ if (!canvas2 || !activeObject) return;
110275
+ setColorRemovalDistance(distPct);
110276
+ if (colorRemovalEnabled) {
110277
+ BackgroundRemovalService.applyColorRemovalFilter(
110278
+ canvas2,
110279
+ activeObject,
110280
+ colorRemovalColor,
110281
+ distPct / 100,
110282
+ false
110283
+ );
110284
+ }
110285
+ };
110286
+ const handleDistanceCommit = () => {
110287
+ saveState();
110288
+ };
109477
110289
  React.useLayoutEffect(() => {
109478
110290
  const newState = {
109479
110291
  ...initialState,
@@ -109502,6 +110314,16 @@ END:VCARD`;
109502
110314
  newState.contrast = { enabled: true, value: Math.round(val * 100) };
109503
110315
  }
109504
110316
  });
110317
+ const rmFilter = obj.filters.find((f) => f.type === "RemoveColor");
110318
+ if (rmFilter) {
110319
+ setColorRemovalEnabled(true);
110320
+ setColorRemovalColor(rmFilter.color || "#FFFFFF");
110321
+ setColorRemovalDistance(Math.round((rmFilter.distance ?? 0.15) * 100));
110322
+ } else {
110323
+ setColorRemovalEnabled(false);
110324
+ }
110325
+ } else {
110326
+ setColorRemovalEnabled(false);
109505
110327
  }
109506
110328
  if (obj.shadow) {
109507
110329
  try {
@@ -109549,6 +110371,105 @@ END:VCARD`;
109549
110371
  { id: "warm", label: "Warm" }
109550
110372
  ];
109551
110373
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto custom-scrollbar overflow-x-hidden px-4 custom-scrollbar", children: [
110374
+ isImg && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-neutral-850 border border-neutral-700/70 rounded-xl p-3 mb-5 space-y-3", children: [
110375
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
110376
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
110377
+ /* @__PURE__ */ jsxRuntime.jsx(BgRemoveIcon, { size: 18, className: "text-[#02bc71]" }),
110378
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-white", children: "Background Removal" })
110379
+ ] }),
110380
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium tracking-wide uppercase px-1.5 py-0.5 rounded bg-[#02bc71]/20 text-[#02bc71] border border-[#02bc71]/30", children: "AI Powered" })
110381
+ ] }),
110382
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
110383
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
110384
+ /* @__PURE__ */ jsxRuntime.jsx(
110385
+ "button",
110386
+ {
110387
+ type: "button",
110388
+ disabled: isRemovingBg,
110389
+ onClick: handleRemoveBgAI,
110390
+ className: `flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded-lg text-xs font-medium transition-all ${isRemovingBg ? "bg-neutral-700 text-neutral-400 cursor-not-allowed" : "bg-[#02bc71] hover:bg-[#029a5d] text-white shadow active:scale-95 cursor-pointer"}`,
110391
+ children: isRemovingBg ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
110392
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "animate-spin h-3.5 w-3.5 text-white", viewBox: "0 0 24 24", fill: "none", children: [
110393
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
110394
+ /* @__PURE__ */ jsxRuntime.jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
110395
+ ] }),
110396
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Processing..." })
110397
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
110398
+ /* @__PURE__ */ jsxRuntime.jsx(BgRemoveIcon, { size: 15 }),
110399
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Cutout Subject (AI)" })
110400
+ ] })
110401
+ }
110402
+ ),
110403
+ hasOriginal && /* @__PURE__ */ jsxRuntime.jsx(
110404
+ "button",
110405
+ {
110406
+ type: "button",
110407
+ disabled: isRemovingBg,
110408
+ onClick: handleRestoreBg,
110409
+ title: "Restore original image background",
110410
+ className: "px-3 py-2 rounded-lg text-xs font-medium bg-neutral-700 hover:bg-neutral-600 text-neutral-200 border border-neutral-600 transition cursor-pointer hover:text-white",
110411
+ children: "Restore"
110412
+ }
110413
+ )
110414
+ ] }),
110415
+ bgProgress && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] text-[#02bc71] italic text-center animate-pulse pt-0.5", children: bgProgress })
110416
+ ] }),
110417
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-neutral-700/50", children: [
110418
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center mb-2", children: [
110419
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-neutral-300 font-medium", children: "Remove Solid Color" }),
110420
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "relative inline-flex items-center cursor-pointer", children: [
110421
+ /* @__PURE__ */ jsxRuntime.jsx(
110422
+ "input",
110423
+ {
110424
+ type: "checkbox",
110425
+ checked: colorRemovalEnabled,
110426
+ onChange: (e2) => handleToggleColorRemoval(e2.target.checked),
110427
+ className: "sr-only peer"
110428
+ }
110429
+ ),
110430
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-4 bg-neutral-700 rounded-full peer-checked:bg-[#02bc71] transition" }),
110431
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition peer-checked:left-4.5" })
110432
+ ] })
110433
+ ] }),
110434
+ colorRemovalEnabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2.5 pt-1", children: [
110435
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
110436
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs opacity-70", children: "Target Color" }),
110437
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
110438
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-neutral-400 font-mono", children: colorRemovalColor.toUpperCase() }),
110439
+ /* @__PURE__ */ jsxRuntime.jsx(
110440
+ CMYKColorPicker,
110441
+ {
110442
+ value: colorRemovalColor,
110443
+ onChange: handleColorChange
110444
+ }
110445
+ )
110446
+ ] })
110447
+ ] }),
110448
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
110449
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center text-xs", children: [
110450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-70", children: "Tolerance / Distance" }),
110451
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-neutral-400", children: [
110452
+ colorRemovalDistance,
110453
+ "%"
110454
+ ] })
110455
+ ] }),
110456
+ /* @__PURE__ */ jsxRuntime.jsx(
110457
+ "input",
110458
+ {
110459
+ type: "range",
110460
+ min: 1,
110461
+ max: 99,
110462
+ value: colorRemovalDistance,
110463
+ onChange: (e2) => handleDistanceChange(Number(e2.target.value)),
110464
+ onMouseUp: handleDistanceCommit,
110465
+ onTouchEnd: handleDistanceCommit,
110466
+ className: "w-full accent-[#02bc71] cursor-pointer"
110467
+ }
110468
+ )
110469
+ ] })
110470
+ ] })
110471
+ ] })
110472
+ ] }),
109552
110473
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-3 mb-6", children: PRESETS.map((p2) => /* @__PURE__ */ jsxRuntime.jsx(
109553
110474
  "button",
109554
110475
  {
@@ -156144,7 +157065,7 @@ ${elements2.join("\n")}
156144
157065
  }
156145
157066
  );
156146
157067
  }
156147
- function AlignLeftIcon() {
157068
+ function AlignLeftIcon$1() {
156148
157069
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 16 16", fill: "currentColor", children: [
156149
157070
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "2", width: "10", height: "2" }),
156150
157071
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "6", width: "14", height: "2" }),
@@ -156152,7 +157073,7 @@ ${elements2.join("\n")}
156152
157073
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "14", width: "14", height: "2" })
156153
157074
  ] });
156154
157075
  }
156155
- function AlignCenterIcon() {
157076
+ function AlignCenterIcon$1() {
156156
157077
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 16 16", fill: "currentColor", children: [
156157
157078
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "2", width: "10", height: "2" }),
156158
157079
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "6", width: "14", height: "2" }),
@@ -156160,7 +157081,7 @@ ${elements2.join("\n")}
156160
157081
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "14", width: "14", height: "2" })
156161
157082
  ] });
156162
157083
  }
156163
- function AlignRightIcon() {
157084
+ function AlignRightIcon$1() {
156164
157085
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 16 16", fill: "currentColor", children: [
156165
157086
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "5", y: "2", width: "10", height: "2" }),
156166
157087
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "6", width: "14", height: "2" }),
@@ -156229,9 +157150,9 @@ ${elements2.join("\n")}
156229
157150
  onClick: () => onChange(next),
156230
157151
  className: "p-1 bg-neutral-800 rounded hover:bg-neutral-700",
156231
157152
  children: [
156232
- value === "left" && /* @__PURE__ */ jsxRuntime.jsx(AlignLeftIcon, {}),
156233
- value === "center" && /* @__PURE__ */ jsxRuntime.jsx(AlignCenterIcon, {}),
156234
- value === "right" && /* @__PURE__ */ jsxRuntime.jsx(AlignRightIcon, {})
157153
+ value === "left" && /* @__PURE__ */ jsxRuntime.jsx(AlignLeftIcon$1, {}),
157154
+ value === "center" && /* @__PURE__ */ jsxRuntime.jsx(AlignCenterIcon$1, {}),
157155
+ value === "right" && /* @__PURE__ */ jsxRuntime.jsx(AlignRightIcon$1, {})
156235
157156
  ]
156236
157157
  }
156237
157158
  );
@@ -156944,8 +157865,13 @@ ${elements2.join("\n")}
156944
157865
  const { type, objects } = useSelectionStore();
156945
157866
  const { canvas: canvas2, startCropping } = useEditorStore();
156946
157867
  const saveState = useHistoryStore((s2) => s2.saveState);
156947
- const hasMask = !!canvas2?.getActiveObject()?.clipPath;
156948
157868
  const setActivePanel = useSidePanelStore((s2) => s2.setActivePanel);
157869
+ const [isRemovingBg, setIsRemovingBg] = React.useState(false);
157870
+ const [bgStatus, setBgStatus] = React.useState(null);
157871
+ const activeObj = canvas2?.getActiveObject();
157872
+ const hasMask = !!activeObj?.clipPath;
157873
+ activeObj && (activeObj.type === "image" || activeObj.isType?.("image"));
157874
+ const isBgRemoved = !!activeObj?.__originalSrc || !!activeObj?.__isBgRemoved;
156949
157875
  if (type !== "image" || objects?.[0]?.name === "qr" || objects?.[0]?.name === "barcode")
156950
157876
  return null;
156951
157877
  const handleApplyMaskClick = () => {
@@ -156968,6 +157894,33 @@ ${elements2.join("\n")}
156968
157894
  startCrop(canvas2, obj);
156969
157895
  startCropping();
156970
157896
  };
157897
+ const handleRemoveBgClick = async () => {
157898
+ if (!canvas2 || !activeObj || isRemovingBg) return;
157899
+ if (isBgRemoved) {
157900
+ setIsRemovingBg(true);
157901
+ try {
157902
+ await BackgroundRemovalService.restoreOriginalBackground(canvas2, activeObj);
157903
+ } finally {
157904
+ setIsRemovingBg(false);
157905
+ }
157906
+ return;
157907
+ }
157908
+ try {
157909
+ setIsRemovingBg(true);
157910
+ setBgStatus("Removing BG...");
157911
+ await BackgroundRemovalService.removeBackgroundAI(
157912
+ canvas2,
157913
+ activeObj,
157914
+ (status) => setBgStatus(status)
157915
+ );
157916
+ } catch (err) {
157917
+ console.error("BG removal failed:", err);
157918
+ alert("Background removal could not be completed: " + (err.message || err));
157919
+ } finally {
157920
+ setIsRemovingBg(false);
157921
+ setBgStatus(null);
157922
+ }
157923
+ };
156971
157924
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-white py-2 rounded-md shadow-lg z-50", children: [
156972
157925
  /* @__PURE__ */ jsxRuntime.jsxs(
156973
157926
  "button",
@@ -156980,14 +157933,27 @@ ${elements2.join("\n")}
156980
157933
  ]
156981
157934
  }
156982
157935
  ),
157936
+ /* @__PURE__ */ jsxRuntime.jsxs(
157937
+ "button",
157938
+ {
157939
+ className: `text-sm px-2 py-[5px] hover:bg-[#8f99a826] hover:rounded-sm flex gap-1.5 items-center cursor-pointer transition-colors ${isBgRemoved ? "text-emerald-400 font-medium" : ""} ${isRemovingBg ? "opacity-75 cursor-wait" : ""}`,
157940
+ onClick: handleRemoveBgClick,
157941
+ disabled: isRemovingBg,
157942
+ title: isBgRemoved ? "Click to restore original background" : "Automatically remove image background with AI",
157943
+ children: [
157944
+ isRemovingBg ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx(BgRemoveIcon, { size: 15 }),
157945
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: isRemovingBg ? bgStatus || "Processing..." : isBgRemoved ? "Restore BG" : "Remove BG" })
157946
+ ]
157947
+ }
157948
+ ),
156983
157949
  /* @__PURE__ */ jsxRuntime.jsx(
156984
157950
  "button",
156985
157951
  {
156986
157952
  className: "text-sm px-1 py-[5px] hover:bg-[#8f99a826] hover:rounded-sm cursor-pointer",
156987
157953
  onClick: () => {
156988
- const canvas22 = useEditorStore.getState().canvas;
156989
- if (!canvas22) return;
156990
- fitImageToPage(canvas22);
157954
+ const cvs = useEditorStore.getState().canvas;
157955
+ if (!cvs) return;
157956
+ fitImageToPage(cvs);
156991
157957
  saveState();
156992
157958
  },
156993
157959
  children: "Fit to page"
@@ -157001,10 +157967,17 @@ ${elements2.join("\n")}
157001
157967
  children: hasMask ? "Remove Mask" : "Apply Mask"
157002
157968
  }
157003
157969
  ),
157004
- /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "text-sm px-1 py-[5px] hover:bg-[#8f99a826] hover:rounded-sm cursor-pointer flex items-center", onClick: handleCropClick, children: [
157005
- /* @__PURE__ */ jsxRuntime.jsx(CropIcon, {}),
157006
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Crop" })
157007
- ] })
157970
+ /* @__PURE__ */ jsxRuntime.jsxs(
157971
+ "button",
157972
+ {
157973
+ className: "text-sm px-1 py-[5px] hover:bg-[#8f99a826] hover:rounded-sm cursor-pointer flex items-center gap-1",
157974
+ onClick: handleCropClick,
157975
+ children: [
157976
+ /* @__PURE__ */ jsxRuntime.jsx(CropIcon, {}),
157977
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Crop" })
157978
+ ]
157979
+ }
157980
+ )
157008
157981
  ] });
157009
157982
  }
157010
157983
  function QrEffectToolbar() {
@@ -157645,6 +158618,102 @@ ${elements2.join("\n")}
157645
158618
  )
157646
158619
  }
157647
158620
  );
158621
+ const AlignTopIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158622
+ "svg",
158623
+ {
158624
+ width: size,
158625
+ height: size,
158626
+ viewBox: "0 0 18 18",
158627
+ fill: "none",
158628
+ xmlns: "http://www.w3.org/2000/svg",
158629
+ className: `shrink-0 ${className}`,
158630
+ children: [
158631
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "14", height: "2", rx: "0.5", fill: "currentColor" }),
158632
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4.5", y: "5.5", width: "2.5", height: "7.5", rx: "0.5", fill: "currentColor" }),
158633
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "10", y: "5.5", width: "2.5", height: "4.5", rx: "0.5", fill: "currentColor" })
158634
+ ]
158635
+ }
158636
+ );
158637
+ const AlignLeftIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158638
+ "svg",
158639
+ {
158640
+ width: size,
158641
+ height: size,
158642
+ viewBox: "0 0 18 18",
158643
+ fill: "none",
158644
+ xmlns: "http://www.w3.org/2000/svg",
158645
+ className: `shrink-0 ${className}`,
158646
+ children: [
158647
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "2", height: "14", rx: "0.5", fill: "currentColor" }),
158648
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "5.5", y: "4.5", width: "7.5", height: "2.5", rx: "0.5", fill: "currentColor" }),
158649
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "5.5", y: "10", width: "4.5", height: "2.5", rx: "0.5", fill: "currentColor" })
158650
+ ]
158651
+ }
158652
+ );
158653
+ const AlignMiddleIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158654
+ "svg",
158655
+ {
158656
+ width: size,
158657
+ height: size,
158658
+ viewBox: "0 0 18 18",
158659
+ fill: "none",
158660
+ xmlns: "http://www.w3.org/2000/svg",
158661
+ className: `shrink-0 ${className}`,
158662
+ children: [
158663
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "9", x2: "16", y2: "9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
158664
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4.5", y: "6", width: "2.5", height: "6", rx: "0.5", fill: "currentColor" }),
158665
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "10", y: "3.5", width: "2.5", height: "11", rx: "0.5", fill: "currentColor" })
158666
+ ]
158667
+ }
158668
+ );
158669
+ const AlignCenterIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158670
+ "svg",
158671
+ {
158672
+ width: size,
158673
+ height: size,
158674
+ viewBox: "0 0 18 18",
158675
+ fill: "none",
158676
+ xmlns: "http://www.w3.org/2000/svg",
158677
+ className: `shrink-0 ${className}`,
158678
+ children: [
158679
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "2", x2: "9", y2: "16", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
158680
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "4.5", width: "6", height: "2.5", rx: "0.5", fill: "currentColor" }),
158681
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3.5", y: "10", width: "11", height: "2.5", rx: "0.5", fill: "currentColor" })
158682
+ ]
158683
+ }
158684
+ );
158685
+ const AlignBottomIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158686
+ "svg",
158687
+ {
158688
+ width: size,
158689
+ height: size,
158690
+ viewBox: "0 0 18 18",
158691
+ fill: "none",
158692
+ xmlns: "http://www.w3.org/2000/svg",
158693
+ className: `shrink-0 ${className}`,
158694
+ children: [
158695
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "14", width: "14", height: "2", rx: "0.5", fill: "currentColor" }),
158696
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4.5", y: "8", width: "2.5", height: "4.5", rx: "0.5", fill: "currentColor" }),
158697
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "10", y: "5", width: "2.5", height: "7.5", rx: "0.5", fill: "currentColor" })
158698
+ ]
158699
+ }
158700
+ );
158701
+ const AlignRightIcon = ({ className = "", size = 18 }) => /* @__PURE__ */ jsxRuntime.jsxs(
158702
+ "svg",
158703
+ {
158704
+ width: size,
158705
+ height: size,
158706
+ viewBox: "0 0 18 18",
158707
+ fill: "none",
158708
+ xmlns: "http://www.w3.org/2000/svg",
158709
+ className: `shrink-0 ${className}`,
158710
+ children: [
158711
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "2", width: "2", height: "14", rx: "0.5", fill: "currentColor" }),
158712
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "8", y: "4.5", width: "4.5", height: "2.5", rx: "0.5", fill: "currentColor" }),
158713
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "5", y: "10", width: "7.5", height: "2.5", rx: "0.5", fill: "currentColor" })
158714
+ ]
158715
+ }
158716
+ );
157648
158717
  function PositionPicker() {
157649
158718
  const canvas2 = useEditorStore((s2) => s2.canvas);
157650
158719
  const saveState = useHistoryStore((s2) => s2.saveState);
@@ -157828,26 +158897,114 @@ ${elements2.join("\n")}
157828
158897
  showPosition && /* @__PURE__ */ jsxRuntime.jsxs(
157829
158898
  "div",
157830
158899
  {
157831
- className: "fixed md:absolute top-[112px] md:top-10 left-1/2 -translate-x-1/2 bg-neutral-800 text-white rounded-xl shadow-xl p-4 w-[260px] z-[9999]",
158900
+ className: "fixed md:absolute top-[112px] md:top-10 left-1/2 -translate-x-1/2 bg-neutral-800 text-white rounded-xl shadow-xl p-4 w-[280px] z-[9999]",
157832
158901
  onClick: (e2) => e2.stopPropagation(),
157833
158902
  onMouseDown: (e2) => e2.stopPropagation(),
157834
158903
  children: [
157835
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase opacity-60 mb-2", children: "Layering" }),
157836
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2 mb-3", children: [
157837
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => bringForward(), children: "Forward" }),
157838
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => sendBackward(), children: "Backward" }),
157839
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => bringToFront(), children: "To Front" }),
157840
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => sendToBack(), children: "To Back" })
158904
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase opacity-60 mb-2 font-medium tracking-wide", children: "Layering" }),
158905
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-1.5 mb-3", children: [
158906
+ /* @__PURE__ */ jsxRuntime.jsx(
158907
+ "button",
158908
+ {
158909
+ onClick: () => bringForward(),
158910
+ className: "flex items-center px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158911
+ children: "Forward"
158912
+ }
158913
+ ),
158914
+ /* @__PURE__ */ jsxRuntime.jsx(
158915
+ "button",
158916
+ {
158917
+ onClick: () => sendBackward(),
158918
+ className: "flex items-center px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158919
+ children: "Backward"
158920
+ }
158921
+ ),
158922
+ /* @__PURE__ */ jsxRuntime.jsx(
158923
+ "button",
158924
+ {
158925
+ onClick: () => bringToFront(),
158926
+ className: "flex items-center px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158927
+ children: "To Front"
158928
+ }
158929
+ ),
158930
+ /* @__PURE__ */ jsxRuntime.jsx(
158931
+ "button",
158932
+ {
158933
+ onClick: () => sendToBack(),
158934
+ className: "flex items-center px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158935
+ children: "To Back"
158936
+ }
158937
+ )
157841
158938
  ] }),
157842
158939
  /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "border-neutral-700 my-2" }),
157843
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase opacity-60 mb-2", children: "Position" }),
157844
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
157845
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("left"), children: "Align left" }),
157846
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("top"), children: "Align top" }),
157847
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("center"), children: "Align center" }),
157848
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("middle"), children: "Align middle" }),
157849
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("right"), children: "Align right" }),
157850
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => handleAlign("bottom"), children: "Align bottom" })
158940
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase opacity-60 mb-2 font-medium tracking-wide", children: "Alignments" }),
158941
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-x-2 gap-y-1.5", children: [
158942
+ /* @__PURE__ */ jsxRuntime.jsxs(
158943
+ "button",
158944
+ {
158945
+ onClick: () => handleAlign("top"),
158946
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158947
+ children: [
158948
+ /* @__PURE__ */ jsxRuntime.jsx(AlignTopIcon, { size: 18 }),
158949
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Top" })
158950
+ ]
158951
+ }
158952
+ ),
158953
+ /* @__PURE__ */ jsxRuntime.jsxs(
158954
+ "button",
158955
+ {
158956
+ onClick: () => handleAlign("left"),
158957
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158958
+ children: [
158959
+ /* @__PURE__ */ jsxRuntime.jsx(AlignLeftIcon, { size: 18 }),
158960
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Left" })
158961
+ ]
158962
+ }
158963
+ ),
158964
+ /* @__PURE__ */ jsxRuntime.jsxs(
158965
+ "button",
158966
+ {
158967
+ onClick: () => handleAlign("middle"),
158968
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158969
+ children: [
158970
+ /* @__PURE__ */ jsxRuntime.jsx(AlignMiddleIcon, { size: 18 }),
158971
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Middle" })
158972
+ ]
158973
+ }
158974
+ ),
158975
+ /* @__PURE__ */ jsxRuntime.jsxs(
158976
+ "button",
158977
+ {
158978
+ onClick: () => handleAlign("center"),
158979
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158980
+ children: [
158981
+ /* @__PURE__ */ jsxRuntime.jsx(AlignCenterIcon, { size: 18 }),
158982
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Center" })
158983
+ ]
158984
+ }
158985
+ ),
158986
+ /* @__PURE__ */ jsxRuntime.jsxs(
158987
+ "button",
158988
+ {
158989
+ onClick: () => handleAlign("bottom"),
158990
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
158991
+ children: [
158992
+ /* @__PURE__ */ jsxRuntime.jsx(AlignBottomIcon, { size: 18 }),
158993
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Bottom" })
158994
+ ]
158995
+ }
158996
+ ),
158997
+ /* @__PURE__ */ jsxRuntime.jsxs(
158998
+ "button",
158999
+ {
159000
+ onClick: () => handleAlign("right"),
159001
+ className: "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm text-gray-200 hover:text-white hover:bg-white/10 transition-colors text-left",
159002
+ children: [
159003
+ /* @__PURE__ */ jsxRuntime.jsx(AlignRightIcon, { size: 18 }),
159004
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Right" })
159005
+ ]
159006
+ }
159007
+ )
157851
159008
  ] })
157852
159009
  ]
157853
159010
  }
@@ -158527,14 +159684,45 @@ ${elements2.join("\n")}
158527
159684
  top: (workarea.top ?? 0) + dy
158528
159685
  });
158529
159686
  workarea.setCoords();
159687
+ if (workarea.clipPath) {
159688
+ workarea.clipPath.set({
159689
+ left: (workarea.clipPath.left ?? 0) + dx,
159690
+ top: (workarea.clipPath.top ?? 0) + dy
159691
+ });
159692
+ workarea.clipPath.setCoords();
159693
+ }
158530
159694
  canvas2.getObjects().forEach((obj) => {
158531
- if (obj.name === "workarea" || obj.name?.startsWith("overlay-")) return;
159695
+ if (obj.name === "workarea" || obj.name?.startsWith("overlay-") || obj.name?.startsWith("guideline-")) {
159696
+ return;
159697
+ }
158532
159698
  obj.set({
158533
159699
  left: (obj.left ?? 0) + dx,
158534
159700
  top: (obj.top ?? 0) + dy
158535
159701
  });
158536
159702
  obj.setCoords();
159703
+ if (obj.clipPath) {
159704
+ obj.clipPath.set({
159705
+ left: (obj.clipPath.left ?? 0) + dx,
159706
+ top: (obj.clipPath.top ?? 0) + dy
159707
+ });
159708
+ obj.clipPath.setCoords();
159709
+ }
158537
159710
  });
159711
+ if (canvas2.backgroundImage) {
159712
+ const bg = canvas2.backgroundImage;
159713
+ bg.set({
159714
+ left: (bg.left ?? 0) + dx,
159715
+ top: (bg.top ?? 0) + dy
159716
+ });
159717
+ bg.setCoords();
159718
+ if (bg.clipPath) {
159719
+ bg.clipPath.set({
159720
+ left: (bg.clipPath.left ?? 0) + dx,
159721
+ top: (bg.clipPath.top ?? 0) + dy
159722
+ });
159723
+ bg.clipPath.setCoords();
159724
+ }
159725
+ }
158538
159726
  createOverlays(canvas2, workarea);
158539
159727
  canvas2.requestRenderAll();
158540
159728
  };
@@ -158560,7 +159748,7 @@ ${elements2.join("\n")}
158560
159748
  canvas2.selection = true;
158561
159749
  canvas2.defaultCursor = "default";
158562
159750
  canvas2.forEachObject((obj) => {
158563
- if (obj.name !== "workarea" && !obj.name?.startsWith("overlay-")) {
159751
+ if (obj.name !== "workarea" && obj.name !== "bg-image" && !obj.name?.startsWith("overlay-") && !obj.name?.startsWith("guideline-")) {
158564
159752
  obj.selectable = true;
158565
159753
  obj.evented = true;
158566
159754
  } else {
@@ -158649,8 +159837,12 @@ ${elements2.join("\n")}
158649
159837
  const containerWidth = container.clientWidth;
158650
159838
  const containerHeight = container.clientHeight;
158651
159839
  const size = productData?.optionsMap?.size?.split(" x ") || ["0", "0"];
158652
- const printWidth = productData?.optionsMap?.productorientation === "Vertical" ? Number(size[0].replace('"', "")) : Number(size[1].replace('"', "")) + Number(productData?.bleedArea || 0);
158653
- const printHeight = productData?.optionsMap?.productorientation === "Vertical" ? Number(size[1].replace('"', "")) : Number(size[0].replace('"', "")) + Number(productData?.bleedArea || 0);
159840
+ const bleed = Number(productData?.bleedArea || 0);
159841
+ const rawW = Number(size[0].replace('"', "")) || 3.5;
159842
+ const rawH = Number(size[1].replace('"', "")) || 2;
159843
+ const isVertical = productData?.optionsMap?.productorientation === "Vertical" || productData?.optionsMap?.orientation === "Vertical";
159844
+ const printWidth = (isVertical ? rawW : rawH) + bleed;
159845
+ const printHeight = (isVertical ? rawH : rawW) + bleed;
158654
159846
  const printDPI = 72;
158655
159847
  const fullWidth = printWidth * printDPI;
158656
159848
  const fullHeight = printHeight * printDPI;
@@ -158681,7 +159873,6 @@ ${elements2.join("\n")}
158681
159873
  );
158682
159874
  const rawShape = productData?.optionsMap?.shape || productData?.optionsMap?.Shape || productData?.shape || "";
158683
159875
  const shapeType = detectShape(rawShape);
158684
- const isVertical = productData?.optionsMap?.productorientation === "Vertical" || productData?.optionsMap?.orientation === "Vertical";
158685
159876
  const { rx, ry, radiusOfCanvas } = calculateShapeRadius(
158686
159877
  shapeType,
158687
159878
  fullWidth,
@@ -158692,6 +159883,8 @@ ${elements2.join("\n")}
158692
159883
  width: fullWidth,
158693
159884
  height: fullHeight,
158694
159885
  fill: "#ffffff",
159886
+ strokeWidth: 0,
159887
+ stroke: null,
158695
159888
  selectable: false,
158696
159889
  evented: false,
158697
159890
  name: "workarea",
@@ -158721,22 +159914,48 @@ ${elements2.join("\n")}
158721
159914
  return workarea;
158722
159915
  }
158723
159916
  function setupHistoryRecording(canvas2) {
158724
- const recordHistory = (e2) => {
159917
+ let modifiedTimer = null;
159918
+ const handleObjectAddedOrRemoved = (e2) => {
158725
159919
  const target = e2?.target;
158726
159920
  if (!target) return;
158727
- if (target.name === "workarea") return;
159921
+ const name = target.name || "";
159922
+ if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-")) {
159923
+ return;
159924
+ }
158728
159925
  if (useHistoryStore.getState().isRestoring) return;
159926
+ if (modifiedTimer) {
159927
+ clearTimeout(modifiedTimer);
159928
+ modifiedTimer = null;
159929
+ }
158729
159930
  requestAnimationFrame(() => {
158730
- useHistoryStore.getState().saveState();
159931
+ if (!useHistoryStore.getState().isRestoring) {
159932
+ useHistoryStore.getState().saveState();
159933
+ }
158731
159934
  });
158732
159935
  };
158733
- canvas2.on("object:added", recordHistory);
158734
- canvas2.on("object:modified", recordHistory);
158735
- canvas2.on("object:removed", recordHistory);
159936
+ const handleObjectModified = (e2) => {
159937
+ const target = e2?.target;
159938
+ if (!target) return;
159939
+ const name = target.name || "";
159940
+ if (name === "workarea" || name === "watermark" || name.startsWith("overlay-") || name.startsWith("guideline-")) {
159941
+ return;
159942
+ }
159943
+ if (useHistoryStore.getState().isRestoring) return;
159944
+ clearTimeout(modifiedTimer);
159945
+ modifiedTimer = setTimeout(() => {
159946
+ if (!useHistoryStore.getState().isRestoring) {
159947
+ useHistoryStore.getState().saveState();
159948
+ }
159949
+ }, 150);
159950
+ };
159951
+ canvas2.on("object:added", handleObjectAddedOrRemoved);
159952
+ canvas2.on("object:removed", handleObjectAddedOrRemoved);
159953
+ canvas2.on("object:modified", handleObjectModified);
158736
159954
  return () => {
158737
- canvas2.off("object:added", recordHistory);
158738
- canvas2.off("object:modified", recordHistory);
158739
- canvas2.off("object:removed", recordHistory);
159955
+ if (modifiedTimer) clearTimeout(modifiedTimer);
159956
+ canvas2.off("object:added", handleObjectAddedOrRemoved);
159957
+ canvas2.off("object:removed", handleObjectAddedOrRemoved);
159958
+ canvas2.off("object:modified", handleObjectModified);
158740
159959
  };
158741
159960
  }
158742
159961
  function setupDoublClickCrop(canvas2) {
@@ -158948,8 +160167,10 @@ ${elements2.join("\n")}
158948
160167
  createWorkarea(canvas2, containerRef.current, productData, targetPage);
158949
160168
  }
158950
160169
  }
158951
- historyStore.saveState();
158952
160170
  useHistoryStore.setState({ isRestoring: false });
160171
+ if ((useHistoryStore.getState().pages[targetPage]?.history?.length ?? 0) === 0) {
160172
+ historyStore.saveState();
160173
+ }
158953
160174
  } finally {
158954
160175
  useLoaderStore.getState().stopLoading();
158955
160176
  }
@@ -158989,9 +160210,62 @@ ${elements2.join("\n")}
158989
160210
  function doReset(canvas2, setZoom) {
158990
160211
  const workarea = getWorkarea(canvas2);
158991
160212
  const initialZoom = workarea ? workarea.__initialZoom || 1 : 1;
158992
- const center = new ot(canvas2.getWidth() / 2, canvas2.getHeight() / 2);
158993
- canvas2.zoomToPoint(center, initialZoom);
160213
+ const canvasW = canvas2.getWidth();
160214
+ const canvasH = canvas2.getHeight();
160215
+ canvas2.setViewportTransform([1, 0, 0, 1, 0, 0]);
160216
+ canvas2.setZoom(1);
158994
160217
  if (workarea) {
160218
+ const prevLeft = workarea.left ?? 0;
160219
+ const prevTop = workarea.top ?? 0;
160220
+ canvas2.centerObject(workarea);
160221
+ workarea.setCoords();
160222
+ const dx = (workarea.left ?? 0) - prevLeft;
160223
+ const dy = (workarea.top ?? 0) - prevTop;
160224
+ if (Math.abs(dx) > 1e-3 || Math.abs(dy) > 1e-3) {
160225
+ canvas2.getObjects().forEach((obj) => {
160226
+ if (obj.name === "workarea" || obj.name?.startsWith("overlay-") || obj.name?.startsWith("guideline-")) {
160227
+ return;
160228
+ }
160229
+ obj.set({
160230
+ left: (obj.left ?? 0) + dx,
160231
+ top: (obj.top ?? 0) + dy
160232
+ });
160233
+ obj.setCoords();
160234
+ if (obj.clipPath) {
160235
+ obj.clipPath.set({
160236
+ left: (obj.clipPath.left ?? 0) + dx,
160237
+ top: (obj.clipPath.top ?? 0) + dy
160238
+ });
160239
+ obj.clipPath.setCoords();
160240
+ }
160241
+ });
160242
+ if (workarea.clipPath) {
160243
+ workarea.clipPath.set({
160244
+ left: (workarea.clipPath.left ?? 0) + dx,
160245
+ top: (workarea.clipPath.top ?? 0) + dy
160246
+ });
160247
+ workarea.clipPath.setCoords();
160248
+ }
160249
+ if (canvas2.backgroundImage) {
160250
+ const bg = canvas2.backgroundImage;
160251
+ bg.set({
160252
+ left: (bg.left ?? 0) + dx,
160253
+ top: (bg.top ?? 0) + dy
160254
+ });
160255
+ bg.setCoords();
160256
+ if (bg.clipPath) {
160257
+ bg.clipPath.set({
160258
+ left: (bg.clipPath.left ?? 0) + dx,
160259
+ top: (bg.clipPath.top ?? 0) + dy
160260
+ });
160261
+ bg.clipPath.setCoords();
160262
+ }
160263
+ }
160264
+ }
160265
+ if (initialZoom !== 1) {
160266
+ const center = new ot(canvasW / 2, canvasH / 2);
160267
+ canvas2.zoomToPoint(center, initialZoom);
160268
+ }
158995
160269
  createOverlays(canvas2, workarea);
158996
160270
  }
158997
160271
  const activeObject = canvas2.getActiveObject();
@@ -158999,7 +160273,7 @@ ${elements2.join("\n")}
158999
160273
  canvas2.requestRenderAll();
159000
160274
  setZoom(initialZoom);
159001
160275
  }
159002
- function useCanvasZoom(canvas2) {
160276
+ function useCanvasZoom(canvas2, options) {
159003
160277
  const [zoom, setZoom] = React.useState(1);
159004
160278
  const rafRef = React.useRef(null);
159005
160279
  const wheelAccum = React.useRef(0);
@@ -159090,12 +160364,13 @@ ${elements2.join("\n")}
159090
160364
  } else if (e2.key === "0") {
159091
160365
  e2.preventDefault();
159092
160366
  doReset(canvas2, setZoom);
160367
+ options?.onReset?.();
159093
160368
  }
159094
160369
  }
159095
160370
  };
159096
160371
  window.addEventListener("keydown", onKey);
159097
160372
  return () => window.removeEventListener("keydown", onKey);
159098
- }, [canvas2, applyZoom]);
160373
+ }, [canvas2, applyZoom, options]);
159099
160374
  const zoomIn = React.useCallback(() => {
159100
160375
  if (!canvas2) return;
159101
160376
  applyZoom(clamp(canvas2.getZoom() + STEP, MIN_ZOOM, MAX_ZOOM));
@@ -159107,7 +160382,8 @@ ${elements2.join("\n")}
159107
160382
  const zoomReset = React.useCallback(() => {
159108
160383
  if (!canvas2) return;
159109
160384
  doReset(canvas2, setZoom);
159110
- }, [canvas2]);
160385
+ options?.onReset?.();
160386
+ }, [canvas2, options]);
159111
160387
  const zoomTo = React.useCallback(
159112
160388
  (value) => applyZoom(clamp(value, MIN_ZOOM, MAX_ZOOM)),
159113
160389
  [applyZoom]
@@ -159122,9 +160398,9 @@ ${elements2.join("\n")}
159122
160398
  zoomTo
159123
160399
  };
159124
160400
  }
159125
- function ZoomControls() {
160401
+ function ZoomControls({ onReset }) {
159126
160402
  const canvas2 = useEditorStore((s2) => s2.canvas);
159127
- const { zoom, atMin, atMax, zoomIn, zoomOut, zoomReset, zoomTo } = useCanvasZoom(canvas2);
160403
+ const { zoom, atMin, atMax, zoomIn, zoomOut, zoomReset, zoomTo } = useCanvasZoom(canvas2, { onReset });
159128
160404
  const [sliderOpen, setSliderOpen] = React.useState(false);
159129
160405
  const sliderRef = React.useRef(null);
159130
160406
  React.useEffect(() => {
@@ -159350,6 +160626,18 @@ ${elements2.join("\n")}
159350
160626
  e2.preventDefault();
159351
160627
  handlePaste();
159352
160628
  }
160629
+ if (isModifier && e2.key.toLowerCase() === "z") {
160630
+ e2.preventDefault();
160631
+ if (e2.shiftKey) {
160632
+ useHistoryStore.getState().redoAction();
160633
+ } else {
160634
+ useHistoryStore.getState().undo();
160635
+ }
160636
+ }
160637
+ if (isModifier && e2.key.toLowerCase() === "y") {
160638
+ e2.preventDefault();
160639
+ useHistoryStore.getState().redoAction();
160640
+ }
159353
160641
  };
159354
160642
  window.addEventListener("keydown", handleKeyDown);
159355
160643
  return () => {
@@ -159466,6 +160754,9 @@ ${elements2.join("\n")}
159466
160754
  pageJSON.current.back = backData;
159467
160755
  }
159468
160756
  }
160757
+ if (useHistoryStore.getState().pages.front.history.length === 0) {
160758
+ useHistoryStore.getState().saveState();
160759
+ }
159469
160760
  useEditorStore.getState().setPages({ ...pageJSON.current });
159470
160761
  return () => {
159471
160762
  cleanupHistory();
@@ -159583,7 +160874,7 @@ ${elements2.join("\n")}
159583
160874
  )
159584
160875
  ] }),
159585
160876
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[1px] h-6 bg-gray-300/50 mx-3" }),
159586
- /* @__PURE__ */ jsxRuntime.jsx(ZoomControls, {})
160877
+ /* @__PURE__ */ jsxRuntime.jsx(ZoomControls, { onReset: () => setIsPanning(false) })
159587
160878
  ] }),
159588
160879
  /* @__PURE__ */ jsxRuntime.jsx(
159589
160880
  "div",