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