embroidery-qc-image 1.0.30 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,6 +33,71 @@ function styleInject(css, ref) {
33
33
  var css_248z = ".render-embroidery {\r\n display: inline-block;\r\n position: relative;\r\n width: 100%;\r\n max-width: 100%;\r\n}\r\n\r\n.render-embroidery-canvas {\r\n display: block;\r\n width: 100%;\r\n height: auto;\r\n image-rendering: high-quality;\r\n background: transparent;\r\n}\r\n";
34
34
  styleInject(css_248z);
35
35
 
36
+ // ============================================================================
37
+ // CONSTANTS
38
+ // ============================================================================
39
+ const COLOR_MAP = {
40
+ "Army (1394)": "#545541",
41
+ Army: "#545541",
42
+ "Black (8)": "#060608",
43
+ Black: "#060608",
44
+ "Bubblegum (1309)": "#E77B9F",
45
+ Bubblegum: "#E77B9F",
46
+ "Carolina Blue (1274)": "#608CC9",
47
+ "Carolina Blue": "#608CC9",
48
+ "Celadon (1098)": "#8EAD8D",
49
+ Celadon: "#8EAD8D",
50
+ "Coffee Bean (1145)": "#502B23",
51
+ "Coffee Bean": "#502B23",
52
+ "Daffodil (1180)": "#FBE30D",
53
+ Daffodil: "#FBE30D",
54
+ "Dark Gray (1131)": "#2E272E",
55
+ "Dark Gray": "#2E272E",
56
+ "Doe Skin Beige (1344)": "#AE9B8B",
57
+ "Doe Skin Beige": "#AE9B8B",
58
+ "Dusty Blue (1373)": "#7B90A9",
59
+ "Dusty Blue": "#7B90A9",
60
+ "Forest Green (1397)": "#073020",
61
+ "Forest Green": "#073020",
62
+ "Gold (1425)": "#D2920A",
63
+ Gold: "#D2920A",
64
+ "Gray (1118)": "#9999A3",
65
+ Gray: "#9999A3",
66
+ "Ivory (1072)": "#E3DAC9",
67
+ Ivory: "#E3DAC9",
68
+ "Lavender (1032)": "#9274B6",
69
+ Lavender: "#9274B6",
70
+ "Light Denim (1133)": "#366696",
71
+ "Light Denim": "#366696",
72
+ "Light Salmon (1018)": "#E0A793",
73
+ "Light Salmon": "#E0A793",
74
+ "Maroon (1374)": "#480C1C",
75
+ Maroon: "#480C1C",
76
+ "Navy Blue (1044)": "#04072A",
77
+ "Navy Blue": "#04072A",
78
+ "Olive Green (1157)": "#625E1F",
79
+ "Olive Green": "#625E1F",
80
+ "Orange (1278)": "#D45D03",
81
+ Orange: "#D45D03",
82
+ "Peach Blush (1053)": "#E2C0B6",
83
+ "Peach Blush": "#E2C0B6",
84
+ "Pink (1148)": "#EFAFBF",
85
+ Pink: "#EFAFBF",
86
+ "Purple (1412)": "#37196F",
87
+ Purple: "#37196F",
88
+ "Red (1037)": "#9D000B",
89
+ Red: "#9D000B",
90
+ "Silver Sage (1396)": "#424F45",
91
+ "Silver Sage": "#424F45",
92
+ "Summer Sky (1432)": "#65A8D2",
93
+ "Summer Sky": "#65A8D2",
94
+ "Terra Cotta (1477)": "#AE3111",
95
+ "Terra Cotta": "#AE3111",
96
+ "Sand (1055)": "#D2C2AB",
97
+ Sand: "#D2C2AB",
98
+ "White (9)": "#D8D7DC",
99
+ White: "#D8D7DC",
100
+ };
36
101
  const DEFAULT_ERROR_COLOR = "#CC1F1A";
37
102
  const DEFAULT_WARNING_COLOR = "#FF8C00";
38
103
  const BASE_URLS = {
@@ -73,7 +138,7 @@ const loadFont = (fontName) => {
73
138
  // Check if font is already loaded in document.fonts (browser cache)
74
139
  const fontFaceSet = document.fonts;
75
140
  for (const font of fontFaceSet) {
76
- if (font.family === fontName && font.status === 'loaded') {
141
+ if (font.family === fontName && font.status === "loaded") {
77
142
  return Promise.resolve();
78
143
  }
79
144
  }
@@ -193,7 +258,9 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
193
258
  target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
194
259
  finalize();
195
260
  };
196
- const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
261
+ const desiredSrc = attemptedProxy
262
+ ? getProxyUrl(getResizeUrl(url))
263
+ : getResizeUrl(url);
197
264
  if (target.src !== desiredSrc) {
198
265
  target.src = desiredSrc;
199
266
  }
@@ -206,22 +273,22 @@ const getResizeUrl = (url) => {
206
273
  try {
207
274
  const urlObj = new URL(url);
208
275
  // Xử lý cdn.shopify.com
209
- if (urlObj.hostname === 'cdn.shopify.com') {
276
+ if (urlObj.hostname === "cdn.shopify.com") {
210
277
  // Set hoặc update query param width=400
211
- urlObj.searchParams.set('width', '400');
278
+ urlObj.searchParams.set("width", "400");
212
279
  return urlObj.toString();
213
280
  }
214
281
  // Xử lý m.media-amazon.com
215
- if (urlObj.hostname === 'm.media-amazon.com') {
282
+ if (urlObj.hostname === "m.media-amazon.com") {
216
283
  const pathname = urlObj.pathname;
217
284
  // Split pathname theo dấu /
218
- const pathArr = pathname.split('/');
285
+ const pathArr = pathname.split("/");
219
286
  // Lấy filename (phần cuối cùng)
220
287
  const filename = pathArr[pathArr.length - 1];
221
288
  // Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
222
- const cleanedFilename = filename.replace(/\._.*_/g, '');
289
+ const cleanedFilename = filename.replace(/\._.*_/g, "");
223
290
  // Split filename đã clean theo dấu .
224
- const parts = cleanedFilename.split('.');
291
+ const parts = cleanedFilename.split(".");
225
292
  if (parts.length >= 2) {
226
293
  // Lấy phần đầu và phần cuối
227
294
  const firstPart = parts[0];
@@ -231,16 +298,16 @@ const getResizeUrl = (url) => {
231
298
  // Thay filename mới vào pathArr
232
299
  pathArr[pathArr.length - 1] = newFilename;
233
300
  // Join lại
234
- urlObj.pathname = pathArr.join('/');
301
+ urlObj.pathname = pathArr.join("/");
235
302
  return urlObj.toString();
236
303
  }
237
304
  }
238
305
  // Xử lý i.etsystatic.com
239
- if (urlObj.hostname === 'i.etsystatic.com') {
306
+ if (urlObj.hostname === "i.etsystatic.com") {
240
307
  const pathname = urlObj.pathname;
241
308
  // Thay il_fullxfull bằng il_400x400
242
- if (pathname.includes('il_fullxfull')) {
243
- const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
309
+ if (pathname.includes("il_fullxfull")) {
310
+ const newPathname = pathname.replace(/il_fullxfull/g, "il_400x400");
244
311
  urlObj.pathname = newPathname;
245
312
  return urlObj.toString();
246
313
  }
@@ -364,7 +431,8 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight, mockupBounds = null) =>
364
431
  let lineToRender = line;
365
432
  if (ctx.measureText(line).width > effectiveMaxWidth) {
366
433
  // Cắt từng ký tự cho đến khi vừa
367
- while (ctx.measureText(lineToRender).width > effectiveMaxWidth && lineToRender.length > 0) {
434
+ while (ctx.measureText(lineToRender).width > effectiveMaxWidth &&
435
+ lineToRender.length > 0) {
368
436
  lineToRender = lineToRender.slice(0, -1);
369
437
  }
370
438
  }
@@ -535,6 +603,262 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
535
603
  // ============================================================================
536
604
  // RENDERING FUNCTIONS
537
605
  // ============================================================================
606
+ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
607
+ // Clear canvas
608
+ ctx.fillStyle = "#e7e7e7";
609
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
610
+ const padding = LAYOUT.PADDING * 6;
611
+ const usableWidth = canvas.width - padding * 2;
612
+ const usableHeight = canvas.height - padding * 2;
613
+ // Calculate sections
614
+ const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
615
+ const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
616
+ const topSectionY = padding;
617
+ const bottomSectionY = topSectionY + topSectionHeight;
618
+ // Get first side (stroke_patches should have only one side)
619
+ const side = config.sides[0];
620
+ if (!side || !side.positions.length) {
621
+ renderErrorState(ctx, canvas, "Không có dữ liệu positions");
622
+ return;
623
+ }
624
+ const position = side.positions[0];
625
+ if (position.type !== "TEXT") {
626
+ renderErrorState(ctx, canvas, "Position phải là TEXT");
627
+ return;
628
+ }
629
+ // Validate required fields
630
+ if (!position.font) {
631
+ renderErrorState(ctx, canvas, "Thiếu font");
632
+ return;
633
+ }
634
+ // Validate layer_colors
635
+ if (!position.layer_colors) {
636
+ renderErrorState(ctx, canvas, "Thiếu layer_colors");
637
+ return;
638
+ }
639
+ if (position.layer_colors.length !== 3) {
640
+ renderErrorState(ctx, canvas, `layer_colors phải có đúng 3 phần tử, hiện tại có ${position.layer_colors.length}`);
641
+ return;
642
+ }
643
+ // Validate từng màu
644
+ const [textColor, borderColor, backgroundColor] = position.layer_colors;
645
+ const missingColors = [];
646
+ if (!textColor || textColor.trim() === "") {
647
+ missingColors.push("Màu text (vị trí 1)");
648
+ }
649
+ if (!borderColor || borderColor.trim() === "") {
650
+ missingColors.push("Màu border (vị trí 2)");
651
+ }
652
+ if (!backgroundColor || backgroundColor.trim() === "") {
653
+ missingColors.push("Màu background (vị trí 3)");
654
+ }
655
+ if (missingColors.length > 0) {
656
+ renderErrorState(ctx, canvas, `Thiếu màu trong layer_colors:\n${missingColors.join("\n")}`);
657
+ return;
658
+ }
659
+ // ============================================================================
660
+ // TOP SECTION (2/3): Hiển thị mẫu preview
661
+ // ============================================================================
662
+ ctx.save();
663
+ // Draw "Hình mẫu:" label at the top
664
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
665
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
666
+ ctx.fillStyle = "#CC0000"; // Red color
667
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
668
+ // Adjust top section Y to account for title + extra spacing (40px)
669
+ const extraSpacing = 40;
670
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
671
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
672
+ // Calculate text size to fit top section
673
+ let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
674
+ ctx.font = `${previewFontSize}px ${position.font}`;
675
+ const text = position.text || "";
676
+ const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
677
+ const maxTextHeight = actualTopSectionHeight; // Use 60% of height
678
+ // Scale down font size to fit
679
+ let textWidth = ctx.measureText(text).width;
680
+ while (textWidth > maxTextWidth && previewFontSize > 50) {
681
+ previewFontSize *= 0.95;
682
+ ctx.font = `${previewFontSize}px ${position.font}`;
683
+ textWidth = ctx.measureText(text).width;
684
+ }
685
+ // Ensure text height also fits
686
+ while (previewFontSize > maxTextHeight && previewFontSize > 50) {
687
+ previewFontSize *= 0.95;
688
+ ctx.font = `${previewFontSize}px ${position.font}`;
689
+ }
690
+ // Update textWidth after final scaling
691
+ textWidth = ctx.measureText(text).width;
692
+ // Center the text in top section
693
+ const textX = padding + usableWidth / 2 - textWidth / 2;
694
+ const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
695
+ // Get color hex values
696
+ const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
697
+ const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
698
+ const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
699
+ // Calculate stroke widths
700
+ // Background needs to be MUCH thicker to create spacing from text
701
+ // Border needs to be even thicker to wrap around background
702
+ const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
703
+ const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
704
+ // Layer 1: Draw border stroke (outermost)
705
+ ctx.strokeStyle = borderColorHex;
706
+ ctx.lineWidth = borderWidth;
707
+ ctx.lineJoin = "round";
708
+ ctx.lineCap = "round";
709
+ ctx.strokeText(text, textX, textY);
710
+ // Layer 2: Draw background color stroke (middle - creates spacing from text)
711
+ ctx.strokeStyle = bgColorHex;
712
+ ctx.lineWidth = bgWidth;
713
+ ctx.lineJoin = "round";
714
+ ctx.lineCap = "round";
715
+ ctx.strokeText(text, textX, textY);
716
+ // Layer 3: Draw text fill ONLY (no stroke on text itself)
717
+ ctx.fillStyle = textColorHex;
718
+ ctx.fillText(text, textX, textY);
719
+ ctx.restore();
720
+ // ============================================================================
721
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
722
+ // ============================================================================
723
+ ctx.save();
724
+ // Draw border around bottom section for debugging
725
+ // ctx.strokeStyle = "#ccc";
726
+ // ctx.lineWidth = 2;
727
+ // ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
728
+ const bottomPadding = 0; // Không padding ngang, sát lề
729
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
730
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
731
+ // Split bottom section: 60% left for info, 40% right for image
732
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
733
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
734
+ const imageSectionX = padding + infoSectionWidth;
735
+ // Left side: Info list
736
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
737
+ const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
738
+ let infoY = bottomSectionY + LAYOUT.PADDING;
739
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
740
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
741
+ ctx.textAlign = "left";
742
+ ctx.textBaseline = "top";
743
+ // Asterisk prefix style
744
+ const drawAsterisk = (x, y) => {
745
+ ctx.save();
746
+ ctx.fillStyle = "#CC0000"; // Red asterisk
747
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
748
+ ctx.fillText("*", x, y);
749
+ ctx.restore();
750
+ };
751
+ const asteriskWidth = ctx.measureText("*").width + 5;
752
+ const startX = padding + asteriskWidth;
753
+ // Font - render "Font: " với font mặc định, tên font với font đó
754
+ drawAsterisk(padding, infoY);
755
+ const fontPrefix = "Font: ";
756
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
757
+ ctx.fillText(fontPrefix, startX, infoY);
758
+ // Render font name với chính font đó
759
+ const prefixWidth = ctx.measureText(fontPrefix).width;
760
+ ctx.font = `${infoFontSize}px ${position.font}`;
761
+ ctx.fillText(position.font, startX + prefixWidth, infoY);
762
+ infoY += infoLineHeight;
763
+ // Reset font về mặc định cho các dòng tiếp theo
764
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
765
+ // Màu chữ (Text Color)
766
+ drawAsterisk(padding, infoY);
767
+ const textColorLabel = `Màu chữ: ${textColor}`;
768
+ ctx.fillText(textColorLabel, startX, infoY);
769
+ // Draw text color swatch
770
+ const swatchSize = infoFontSize * 1.3; // Giảm từ 1.5 xuống 1.3
771
+ const swatchX = startX +
772
+ ctx.measureText(textColorLabel).width +
773
+ LAYOUT.ELEMENT_SPACING * 0.3; // Giảm spacing
774
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
775
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
776
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
777
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
778
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
779
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
780
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
781
+ }
782
+ infoY += infoLineHeight;
783
+ // Màu nền (Background Color)
784
+ drawAsterisk(padding + bottomPadding, infoY);
785
+ const bgColorLabel = `Màu nền: ${backgroundColor}`;
786
+ ctx.fillText(bgColorLabel, startX, infoY);
787
+ // Draw background color swatch
788
+ const bgSwatchX = startX + ctx.measureText(bgColorLabel).width + LAYOUT.ELEMENT_SPACING * 0.3;
789
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
790
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
791
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
792
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
793
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
794
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
795
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
796
+ }
797
+ infoY += infoLineHeight;
798
+ // Màu viền (Border Color)
799
+ drawAsterisk(padding + bottomPadding, infoY);
800
+ const borderColorLabel = `Màu viền: ${borderColor}`;
801
+ ctx.fillText(borderColorLabel, startX, infoY);
802
+ // Draw border color swatch
803
+ const borderSwatchX = startX +
804
+ ctx.measureText(borderColorLabel).width +
805
+ LAYOUT.ELEMENT_SPACING * 0.3;
806
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
807
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
808
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
809
+ if (borderColorSwatchImg?.complete &&
810
+ borderColorSwatchImg.naturalHeight > 0) {
811
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
812
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
813
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
814
+ }
815
+ infoY += infoLineHeight;
816
+ // Attachment
817
+ if (position.attachment) {
818
+ drawAsterisk(padding + bottomPadding, infoY);
819
+ const attachmentLabel = `Attachment: ${position.attachment}`;
820
+ ctx.fillText(attachmentLabel, startX, infoY);
821
+ infoY += infoLineHeight;
822
+ }
823
+ // Size
824
+ if (side.size) {
825
+ drawAsterisk(padding + bottomPadding, infoY);
826
+ const sizeLabel = `Size: ${side.size}`;
827
+ ctx.fillText(sizeLabel, startX, infoY);
828
+ infoY += infoLineHeight;
829
+ }
830
+ // Right side: Image from config.image_url
831
+ if (config.image_url) {
832
+ // Draw "Mockup" label
833
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
834
+ ctx.fillStyle = "#000000";
835
+ const mockupLabel = "Mockup";
836
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
837
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
838
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
839
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
840
+ const img = imageRefs.current.get(config.image_url) ??
841
+ imageRefs.current.get("mockup");
842
+ if (img?.complete && img.naturalWidth > 0) {
843
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
844
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
845
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
846
+ let drawWidth = maxImgWidth;
847
+ let drawHeight = drawWidth / imgAspectRatio;
848
+ if (drawHeight > maxImgHeight) {
849
+ drawHeight = maxImgHeight;
850
+ drawWidth = drawHeight * imgAspectRatio;
851
+ }
852
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
853
+ const imgY = bottomSectionY +
854
+ LAYOUT.PADDING +
855
+ mockupLabelHeight +
856
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
857
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
858
+ }
859
+ }
860
+ ctx.restore();
861
+ };
538
862
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
539
863
  const ctx = canvas.getContext("2d");
540
864
  if (!ctx)
@@ -549,6 +873,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
549
873
  }
550
874
  if (!config.sides?.length)
551
875
  return;
876
+ // Check if this is a stroke_patches layout
877
+ const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
878
+ if (hasStrokePatches) {
879
+ renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
880
+ return;
881
+ }
552
882
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
553
883
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
554
884
  if (config.image_url) {
@@ -721,7 +1051,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
721
1051
  // Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
722
1052
  const lineTopY = y;
723
1053
  const lineBottomY = y + lineHeight;
724
- const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
1054
+ const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
1055
+ lineBottomY > mockupBounds.y;
725
1056
  if (overlapsY) {
726
1057
  // Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
727
1058
  const maxAllowedWidth = mockupBounds.x - x;
@@ -768,15 +1099,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
768
1099
  const iconPositions = side.positions.filter((p) => p.type === "ICON");
769
1100
  // Kiểm tra tất cả TEXT positions có trống không
770
1101
  // Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
771
- const allTextEmpty = textPositions.length === 0 || textPositions.every((p) => {
772
- const text = p.text ?? "";
773
- return text.trim() === "";
774
- });
1102
+ const allTextEmpty = textPositions.length === 0 ||
1103
+ textPositions.every((p) => {
1104
+ const text = p.text ?? "";
1105
+ return text.trim() === "";
1106
+ });
775
1107
  // Kiểm tra tất cả ICON positions có is_delete_icon = true không
776
1108
  // Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
777
- const allIconsDeleted = iconPositions.length === 0 || iconPositions.every((p) => {
778
- return p.is_delete_icon === true;
779
- });
1109
+ const allIconsDeleted = iconPositions.length === 0 ||
1110
+ iconPositions.every((p) => {
1111
+ return p.is_delete_icon === true;
1112
+ });
780
1113
  // Nếu tất cả TEXT trống và tất cả ICON bị xóa, chỉ render dòng "(không thêu gì)"
781
1114
  if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
782
1115
  ctx.save();
@@ -790,7 +1123,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
790
1123
  return currentY - startY;
791
1124
  }
792
1125
  // Compute uniform properties
793
- const iconColorPositions = side.positions.filter((p) => p.type === "ICON" && (!p.layer_colors?.length || p.layer_colors.length === 1));
1126
+ const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
1127
+ (!p.layer_colors?.length || p.layer_colors.length === 1));
794
1128
  const iconColorValues = iconColorPositions
795
1129
  .map((p) => {
796
1130
  if (p.layer_colors?.length === 1)
@@ -912,7 +1246,8 @@ const computeUniformProperties = (textPositions, options) => {
912
1246
  ...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
913
1247
  ];
914
1248
  if (textPositions.length === 0 &&
915
- (!options?.additionalColorValues || options.additionalColorValues.length === 0)) {
1249
+ (!options?.additionalColorValues ||
1250
+ options.additionalColorValues.length === 0)) {
916
1251
  return defaults;
917
1252
  }
918
1253
  const colors = new Set(colorSources);
@@ -1013,7 +1348,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
1013
1348
  }
1014
1349
  rendered++;
1015
1350
  }
1016
- if (values.floral && values.floral !== "None" && shouldRenderField("floral")) {
1351
+ if (values.floral &&
1352
+ values.floral !== "None" &&
1353
+ shouldRenderField("floral")) {
1017
1354
  const floralUrl = getImageUrl("floral", values.floral);
1018
1355
  const floralImg = imageRefs.current.get(floralUrl);
1019
1356
  // Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
@@ -1077,9 +1414,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1077
1414
  let currentY = y;
1078
1415
  // Chuẩn hóa xuống dòng:
1079
1416
  // - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
1080
- const normalizeNewlines = (text) => text
1081
- .replace(/\r\n/g, "\n")
1082
- .replace(/\r/g, "\n");
1417
+ const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1083
1418
  // Get display text (handle empty/null/undefined) sau khi normalize
1084
1419
  const rawOriginalText = position.text ?? "";
1085
1420
  const normalizedText = normalizeNewlines(rawOriginalText);
@@ -1116,7 +1451,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1116
1451
  let minShrinkRatio = 1;
1117
1452
  ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
1118
1453
  lines.forEach((line, idx) => {
1119
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1454
+ const lineY = textCenterY -
1455
+ ((lines.length - 1) / 2) * valueLineHeight +
1456
+ idx * valueLineHeight;
1120
1457
  // Tính effectiveMaxWidth cho dòng này
1121
1458
  const effectiveMaxWidth = mockupBounds
1122
1459
  ? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
@@ -1144,7 +1481,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1144
1481
  // Vẽ từ trên xuống: căn giữa mỗi dòng
1145
1482
  // Font size đã được tính để vừa với effectiveMaxWidth của từng dòng, nên không cần cắt text
1146
1483
  lines.forEach((line, idx) => {
1147
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1484
+ const lineY = textCenterY -
1485
+ ((lines.length - 1) / 2) * valueLineHeight +
1486
+ idx * valueLineHeight;
1148
1487
  ctx.fillText(line, valueStartX, lineY);
1149
1488
  });
1150
1489
  // Reset textBaseline về top cho các phần tiếp theo
@@ -1231,7 +1570,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1231
1570
  let lineToRender = line;
1232
1571
  const lineWidth = ctx.measureText(lineToRender).width;
1233
1572
  if (lineWidth > lineEffectiveMaxWidth) {
1234
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1573
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1574
+ lineToRender.length > 0) {
1235
1575
  lineToRender = lineToRender.slice(0, -1);
1236
1576
  }
1237
1577
  }
@@ -1256,7 +1596,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1256
1596
  if (totalWidth > shrunkEffectiveMaxWidth) {
1257
1597
  // Cần cắt font name
1258
1598
  let truncatedFontName = fontName;
1259
- while (ctx.measureText(truncatedFontName).width > shrunkEffectiveMaxWidth - prefixWidth && truncatedFontName.length > 0) {
1599
+ while (ctx.measureText(truncatedFontName).width >
1600
+ shrunkEffectiveMaxWidth - prefixWidth &&
1601
+ truncatedFontName.length > 0) {
1260
1602
  truncatedFontName = truncatedFontName.slice(0, -1);
1261
1603
  }
1262
1604
  ctx.fillText(truncatedFontName, currentX, currentY);
@@ -1270,7 +1612,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1270
1612
  const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
1271
1613
  if (remainingWidth > 0) {
1272
1614
  let truncatedSuffix = suffix;
1273
- while (ctx.measureText(truncatedSuffix).width > remainingWidth && truncatedSuffix.length > 0) {
1615
+ while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
1616
+ truncatedSuffix.length > 0) {
1274
1617
  truncatedSuffix = truncatedSuffix.slice(0, -1);
1275
1618
  }
1276
1619
  if (truncatedSuffix.length > 0) {
@@ -1311,7 +1654,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1311
1654
  else {
1312
1655
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1313
1656
  swatchX = swatchesStartX;
1314
- swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1657
+ swatchY =
1658
+ result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1315
1659
  currentY += result.height;
1316
1660
  }
1317
1661
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
@@ -1420,7 +1764,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1420
1764
  const ratio = img.naturalWidth / img.naturalHeight;
1421
1765
  const iconHeight = iconFontSize * 2;
1422
1766
  const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
1423
- iconImageReservedWidth = iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1767
+ iconImageReservedWidth =
1768
+ iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1424
1769
  }
1425
1770
  }
1426
1771
  }
@@ -1498,7 +1843,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1498
1843
  let lineToRender = line;
1499
1844
  const lineWidth = ctx.measureText(lineToRender).width;
1500
1845
  if (lineWidth > lineEffectiveMaxWidth) {
1501
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1846
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1847
+ lineToRender.length > 0) {
1502
1848
  lineToRender = lineToRender.slice(0, -1);
1503
1849
  }
1504
1850
  }
@@ -1519,7 +1865,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1519
1865
  let lineToRender = lineText;
1520
1866
  const lineWidth = ctx.measureText(lineToRender).width;
1521
1867
  if (lineWidth > shrunkEffectiveMaxWidth) {
1522
- while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth && lineToRender.length > 0) {
1868
+ while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
1869
+ lineToRender.length > 0) {
1523
1870
  lineToRender = lineToRender.slice(0, -1);
1524
1871
  }
1525
1872
  }
@@ -1582,7 +1929,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1582
1929
  const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
1583
1930
  testLines.length * lineHeight;
1584
1931
  // Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
1585
- const testTextWidth = Math.max(...testLines.map(line => ctx.measureText(line).width));
1932
+ const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
1586
1933
  const textEndX = x + testTextWidth;
1587
1934
  const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1588
1935
  const swatchesStartX = textEndX + spacing;
@@ -1611,7 +1958,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1611
1958
  else {
1612
1959
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1613
1960
  swatchX = swatchesStartX;
1614
- swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1961
+ swatchY =
1962
+ colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1615
1963
  // Render swatches (đã kiểm tra overflow ở trên)
1616
1964
  drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
1617
1965
  // cursorY đã được cập nhật ở trên
@@ -1627,10 +1975,7 @@ const prepareExportCanvas = async (config, options = {}) => {
1627
1975
  current: new Map(),
1628
1976
  };
1629
1977
  // Load fonts and images in parallel
1630
- await Promise.all([
1631
- preloadFonts(config),
1632
- preloadImages(config, imageRefs)
1633
- ]);
1978
+ await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
1634
1979
  renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
1635
1980
  if (!canvas.width || !canvas.height) {
1636
1981
  return null;