embroidery-qc-image 1.0.30 → 1.0.32

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,324 @@ 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
+ // Get layer colors with empty check (don't use fallback)
630
+ const textColor = position.layer_colors?.[0];
631
+ const borderColor = position.layer_colors?.[1];
632
+ const backgroundColor = position.layer_colors?.[2];
633
+ const fabricColor = position.layer_colors?.[3]; // Màu vải
634
+ // For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
635
+ const textColorForRender = textColor;
636
+ const borderColorForRender = borderColor;
637
+ const backgroundColorForRender = backgroundColor;
638
+ // Check if font is missing (but continue rendering)
639
+ const isFontMissing = !position.font || position.font.trim() === "";
640
+ // ============================================================================
641
+ // TOP SECTION (2/3): Hiển thị mẫu preview
642
+ // ============================================================================
643
+ ctx.save();
644
+ // Draw "Hình mẫu:" label at the top
645
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
646
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
647
+ ctx.fillStyle = "#CC0000"; // Red color
648
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
649
+ // Adjust top section Y to account for title + extra spacing (40px)
650
+ const extraSpacing = 40;
651
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
652
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
653
+ // Calculate text size to fit top section
654
+ let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
655
+ const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
656
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
657
+ const text = position.text || "";
658
+ const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
659
+ const maxTextHeight = actualTopSectionHeight; // Use 60% of height
660
+ // Scale down font size to fit
661
+ let textWidth = ctx.measureText(text).width;
662
+ while (textWidth > maxTextWidth && previewFontSize > 50) {
663
+ previewFontSize *= 0.95;
664
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
665
+ textWidth = ctx.measureText(text).width;
666
+ }
667
+ // Ensure text height also fits
668
+ while (previewFontSize > maxTextHeight && previewFontSize > 50) {
669
+ previewFontSize *= 0.95;
670
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
671
+ }
672
+ // Update textWidth after final scaling
673
+ textWidth = ctx.measureText(text).width;
674
+ // Center the text in top section
675
+ const textX = padding + usableWidth / 2 - textWidth / 2;
676
+ const textY = actualTopSectionY + actualTopSectionHeight / 2 - previewFontSize / 2;
677
+ // Get color hex values (use render colors with fallback)
678
+ const textColorHex = COLOR_MAP[textColorForRender] || LAYOUT.LABEL_COLOR;
679
+ const borderColorHex = COLOR_MAP[borderColorForRender] || LAYOUT.LABEL_COLOR;
680
+ const bgColorHex = COLOR_MAP[backgroundColorForRender] || "#FFFFFF";
681
+ // Calculate stroke widths
682
+ // Background needs to be MUCH thicker to create spacing from text
683
+ // Border needs to be even thicker to wrap around background
684
+ const bgWidth = Math.max(80, previewFontSize / 2); // Background - rất dày để tạo khoảng cách lớn
685
+ const borderWidth = Math.max(100, previewFontSize / 1.58); // Border - dày hơn để bọc background
686
+ // Layer 1: Draw border stroke (outermost)
687
+ ctx.strokeStyle = borderColorHex;
688
+ ctx.lineWidth = borderWidth;
689
+ ctx.lineJoin = "round";
690
+ ctx.lineCap = "round";
691
+ ctx.strokeText(text, textX, textY);
692
+ // Layer 2: Draw background color stroke (middle - creates spacing from text)
693
+ ctx.strokeStyle = bgColorHex;
694
+ ctx.lineWidth = bgWidth;
695
+ ctx.lineJoin = "round";
696
+ ctx.lineCap = "round";
697
+ ctx.strokeText(text, textX, textY);
698
+ // Layer 3: Draw text fill ONLY (no stroke on text itself)
699
+ ctx.fillStyle = textColorHex;
700
+ ctx.fillText(text, textX, textY);
701
+ ctx.restore();
702
+ // ============================================================================
703
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
704
+ // ============================================================================
705
+ ctx.save();
706
+ // Draw border around bottom section for debugging
707
+ // ctx.strokeStyle = "#ccc";
708
+ // ctx.lineWidth = 2;
709
+ // ctx.strokeRect(padding, bottomSectionY, usableWidth, bottomSectionHeight);
710
+ const bottomPadding = 0; // Không padding ngang, sát lề
711
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
712
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
713
+ // Split bottom section: 60% left for info, 40% right for image
714
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
715
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
716
+ const imageSectionX = padding + infoSectionWidth;
717
+ // Left side: Info list
718
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9; // Giảm từ 1.2 xuống 0.9
719
+ const infoLineHeight = infoFontSize * 1.4; // Giảm từ 1.5 xuống 1.4
720
+ let infoY = bottomSectionY + LAYOUT.PADDING;
721
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
722
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
723
+ ctx.textAlign = "left";
724
+ ctx.textBaseline = "top";
725
+ // Asterisk prefix style
726
+ const drawAsterisk = (x, y) => {
727
+ ctx.save();
728
+ ctx.fillStyle = "#CC0000"; // Red asterisk
729
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
730
+ ctx.fillText("*", x, y);
731
+ ctx.restore();
732
+ };
733
+ const asteriskWidth = ctx.measureText("*").width + 5;
734
+ const startX = padding + asteriskWidth;
735
+ // Font - render "Font: " với font mặc định, tên font với font đó
736
+ drawAsterisk(padding, infoY);
737
+ const fontPrefix = "Font: ";
738
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
739
+ ctx.fillText(fontPrefix, startX, infoY);
740
+ if (isFontMissing) {
741
+ // Hiển thị warning màu đỏ nếu thiếu font
742
+ ctx.fillStyle = "#CC0000"; // Red color
743
+ ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
744
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
745
+ }
746
+ else {
747
+ // Render font name với chính font đó
748
+ const prefixWidth = ctx.measureText(fontPrefix).width;
749
+ const fontName = position.font || LAYOUT.FONT_FAMILY;
750
+ ctx.font = `${infoFontSize}px ${fontName}`;
751
+ ctx.fillText(fontName, startX + prefixWidth, infoY);
752
+ }
753
+ infoY += infoLineHeight;
754
+ // Reset font về mặc định cho các dòng tiếp theo
755
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
756
+ // Màu chữ (Text Color) - layer_colors[0]
757
+ drawAsterisk(padding, infoY);
758
+ const textColorPrefix = "Màu chữ: ";
759
+ ctx.fillText(textColorPrefix, startX, infoY);
760
+ if (!textColor || textColor.trim() === "") {
761
+ // Hiển thị warning màu đỏ nếu thiếu màu
762
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
763
+ ctx.fillStyle = "#CC0000";
764
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
765
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
766
+ }
767
+ else {
768
+ // Hiển thị tên màu
769
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
770
+ ctx.fillText(textColor, startX + prefixWidth, infoY);
771
+ // Draw text color swatch
772
+ const swatchSize = infoFontSize * 1.3;
773
+ const swatchX = startX +
774
+ ctx.measureText(textColorPrefix + textColor).width +
775
+ LAYOUT.ELEMENT_SPACING * 0.3;
776
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
777
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
778
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
779
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
780
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
781
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
782
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
783
+ }
784
+ }
785
+ infoY += infoLineHeight;
786
+ // Màu nền (Background Color) - layer_colors[2]
787
+ drawAsterisk(padding, infoY);
788
+ const bgColorPrefix = "Màu nền: ";
789
+ ctx.fillText(bgColorPrefix, startX, infoY);
790
+ if (!backgroundColor || backgroundColor.trim() === "") {
791
+ // Hiển thị warning màu đỏ nếu thiếu màu
792
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
793
+ ctx.fillStyle = "#CC0000";
794
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
795
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
796
+ }
797
+ else {
798
+ // Hiển thị tên màu
799
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
800
+ ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
801
+ // Draw background color swatch
802
+ const swatchSize = infoFontSize * 1.3;
803
+ const bgSwatchX = startX +
804
+ ctx.measureText(bgColorPrefix + backgroundColor).width +
805
+ LAYOUT.ELEMENT_SPACING * 0.3;
806
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
807
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
808
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
809
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
810
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
811
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
812
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
813
+ }
814
+ }
815
+ infoY += infoLineHeight;
816
+ // Màu viền (Border Color) - layer_colors[1]
817
+ drawAsterisk(padding, infoY);
818
+ const borderColorPrefix = "Màu viền: ";
819
+ ctx.fillText(borderColorPrefix, startX, infoY);
820
+ if (!borderColor || borderColor.trim() === "") {
821
+ // Hiển thị warning màu đỏ nếu thiếu màu
822
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
823
+ ctx.fillStyle = "#CC0000";
824
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
825
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
826
+ }
827
+ else {
828
+ // Hiển thị tên màu
829
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
830
+ ctx.fillText(borderColor, startX + prefixWidth, infoY);
831
+ // Draw border color swatch
832
+ const swatchSize = infoFontSize * 1.3;
833
+ const borderSwatchX = startX +
834
+ ctx.measureText(borderColorPrefix + borderColor).width +
835
+ LAYOUT.ELEMENT_SPACING * 0.3;
836
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
837
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
838
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
839
+ if (borderColorSwatchImg?.complete &&
840
+ borderColorSwatchImg.naturalHeight > 0) {
841
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
842
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
843
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
844
+ }
845
+ }
846
+ infoY += infoLineHeight;
847
+ // Màu vải (Fabric Color) - layer_colors[3]
848
+ drawAsterisk(padding, infoY);
849
+ const fabricColorPrefix = "Màu vải: ";
850
+ ctx.fillText(fabricColorPrefix, startX, infoY);
851
+ if (!fabricColor || fabricColor.trim() === "") {
852
+ // Hiển thị warning màu đỏ nếu thiếu màu
853
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
854
+ ctx.fillStyle = "#CC0000";
855
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
856
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
857
+ }
858
+ else {
859
+ // Hiển thị tên màu
860
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
861
+ ctx.fillText(fabricColor, startX + prefixWidth, infoY);
862
+ // Draw fabric color swatch
863
+ const swatchSize = infoFontSize * 1.3;
864
+ const fabricSwatchX = startX +
865
+ ctx.measureText(fabricColorPrefix + fabricColor).width +
866
+ LAYOUT.ELEMENT_SPACING * 0.3;
867
+ const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
868
+ const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
869
+ const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
870
+ if (fabricColorSwatchImg?.complete &&
871
+ fabricColorSwatchImg.naturalHeight > 0) {
872
+ const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
873
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
874
+ ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
875
+ }
876
+ }
877
+ infoY += infoLineHeight;
878
+ // Attachment
879
+ if (position.attachment) {
880
+ drawAsterisk(padding + bottomPadding, infoY);
881
+ const attachmentLabel = `Attachment: ${position.attachment}`;
882
+ ctx.fillText(attachmentLabel, startX, infoY);
883
+ infoY += infoLineHeight;
884
+ }
885
+ // Size
886
+ if (side.size) {
887
+ drawAsterisk(padding + bottomPadding, infoY);
888
+ const sizeLabel = `Size: ${side.size}`;
889
+ ctx.fillText(sizeLabel, startX, infoY);
890
+ infoY += infoLineHeight;
891
+ }
892
+ // Right side: Image from config.image_url
893
+ if (config.image_url) {
894
+ // Draw "Mockup" label
895
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
896
+ ctx.fillStyle = "#000000";
897
+ const mockupLabel = "Mockup";
898
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
899
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
900
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
901
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
902
+ const img = imageRefs.current.get(config.image_url) ??
903
+ imageRefs.current.get("mockup");
904
+ if (img?.complete && img.naturalWidth > 0) {
905
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
906
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
907
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
908
+ let drawWidth = maxImgWidth;
909
+ let drawHeight = drawWidth / imgAspectRatio;
910
+ if (drawHeight > maxImgHeight) {
911
+ drawHeight = maxImgHeight;
912
+ drawWidth = drawHeight * imgAspectRatio;
913
+ }
914
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
915
+ const imgY = bottomSectionY +
916
+ LAYOUT.PADDING +
917
+ mockupLabelHeight +
918
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
919
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
920
+ }
921
+ }
922
+ ctx.restore();
923
+ };
538
924
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
539
925
  const ctx = canvas.getContext("2d");
540
926
  if (!ctx)
@@ -549,6 +935,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
549
935
  }
550
936
  if (!config.sides?.length)
551
937
  return;
938
+ // Check if this is a stroke_patches layout
939
+ const hasStrokePatches = config.sides.some((side) => side.item_type && side.item_type.includes("stroke_patches"));
940
+ if (hasStrokePatches) {
941
+ renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
942
+ return;
943
+ }
552
944
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
553
945
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
554
946
  if (config.image_url) {
@@ -721,7 +1113,8 @@ const getEffectiveMaxWidth = (x, y, lineHeight, originalMaxWidth, mockupBounds)
721
1113
  // Kiểm tra xem dòng text có nằm trong phạm vi Y của mockup không
722
1114
  const lineTopY = y;
723
1115
  const lineBottomY = y + lineHeight;
724
- const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height && lineBottomY > mockupBounds.y;
1116
+ const overlapsY = lineTopY < mockupBounds.y + mockupBounds.height &&
1117
+ lineBottomY > mockupBounds.y;
725
1118
  if (overlapsY) {
726
1119
  // Nếu overlap theo Y, giới hạn maxWidth để text không vượt quá mockup.x
727
1120
  const maxAllowedWidth = mockupBounds.x - x;
@@ -768,15 +1161,17 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
768
1161
  const iconPositions = side.positions.filter((p) => p.type === "ICON");
769
1162
  // Kiểm tra tất cả TEXT positions có trống không
770
1163
  // 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
- });
1164
+ const allTextEmpty = textPositions.length === 0 ||
1165
+ textPositions.every((p) => {
1166
+ const text = p.text ?? "";
1167
+ return text.trim() === "";
1168
+ });
775
1169
  // Kiểm tra tất cả ICON positions có is_delete_icon = true không
776
1170
  // 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
- });
1171
+ const allIconsDeleted = iconPositions.length === 0 ||
1172
+ iconPositions.every((p) => {
1173
+ return p.is_delete_icon === true;
1174
+ });
780
1175
  // 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
1176
  if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
782
1177
  ctx.save();
@@ -790,7 +1185,8 @@ const renderSide = (ctx, side, startY, width, height, scaleFactor, imageRefs, mo
790
1185
  return currentY - startY;
791
1186
  }
792
1187
  // Compute uniform properties
793
- const iconColorPositions = side.positions.filter((p) => p.type === "ICON" && (!p.layer_colors?.length || p.layer_colors.length === 1));
1188
+ const iconColorPositions = side.positions.filter((p) => p.type === "ICON" &&
1189
+ (!p.layer_colors?.length || p.layer_colors.length === 1));
794
1190
  const iconColorValues = iconColorPositions
795
1191
  .map((p) => {
796
1192
  if (p.layer_colors?.length === 1)
@@ -912,7 +1308,8 @@ const computeUniformProperties = (textPositions, options) => {
912
1308
  ...(options?.additionalColorValues?.map((color) => color ?? "None") ?? []),
913
1309
  ];
914
1310
  if (textPositions.length === 0 &&
915
- (!options?.additionalColorValues || options.additionalColorValues.length === 0)) {
1311
+ (!options?.additionalColorValues ||
1312
+ options.additionalColorValues.length === 0)) {
916
1313
  return defaults;
917
1314
  }
918
1315
  const colors = new Set(colorSources);
@@ -1013,7 +1410,9 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
1013
1410
  }
1014
1411
  rendered++;
1015
1412
  }
1016
- if (values.floral && values.floral !== "None" && shouldRenderField("floral")) {
1413
+ if (values.floral &&
1414
+ values.floral !== "None" &&
1415
+ shouldRenderField("floral")) {
1017
1416
  const floralUrl = getImageUrl("floral", values.floral);
1018
1417
  const floralImg = imageRefs.current.get(floralUrl);
1019
1418
  // Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
@@ -1077,9 +1476,7 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1077
1476
  let currentY = y;
1078
1477
  // Chuẩn hóa xuống dòng:
1079
1478
  // - 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");
1479
+ const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1083
1480
  // Get display text (handle empty/null/undefined) sau khi normalize
1084
1481
  const rawOriginalText = position.text ?? "";
1085
1482
  const normalizedText = normalizeNewlines(rawOriginalText);
@@ -1116,7 +1513,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1116
1513
  let minShrinkRatio = 1;
1117
1514
  ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
1118
1515
  lines.forEach((line, idx) => {
1119
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1516
+ const lineY = textCenterY -
1517
+ ((lines.length - 1) / 2) * valueLineHeight +
1518
+ idx * valueLineHeight;
1120
1519
  // Tính effectiveMaxWidth cho dòng này
1121
1520
  const effectiveMaxWidth = mockupBounds
1122
1521
  ? getEffectiveMaxWidth(valueStartX, lineY, valueLineHeight, availableWidth, mockupBounds)
@@ -1144,7 +1543,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1144
1543
  // Vẽ từ trên xuống: căn giữa mỗi dòng
1145
1544
  // 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
1545
  lines.forEach((line, idx) => {
1147
- const lineY = textCenterY - (lines.length - 1) / 2 * valueLineHeight + idx * valueLineHeight;
1546
+ const lineY = textCenterY -
1547
+ ((lines.length - 1) / 2) * valueLineHeight +
1548
+ idx * valueLineHeight;
1148
1549
  ctx.fillText(line, valueStartX, lineY);
1149
1550
  });
1150
1551
  // Reset textBaseline về top cho các phần tiếp theo
@@ -1231,7 +1632,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1231
1632
  let lineToRender = line;
1232
1633
  const lineWidth = ctx.measureText(lineToRender).width;
1233
1634
  if (lineWidth > lineEffectiveMaxWidth) {
1234
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1635
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1636
+ lineToRender.length > 0) {
1235
1637
  lineToRender = lineToRender.slice(0, -1);
1236
1638
  }
1237
1639
  }
@@ -1256,7 +1658,9 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1256
1658
  if (totalWidth > shrunkEffectiveMaxWidth) {
1257
1659
  // Cần cắt font name
1258
1660
  let truncatedFontName = fontName;
1259
- while (ctx.measureText(truncatedFontName).width > shrunkEffectiveMaxWidth - prefixWidth && truncatedFontName.length > 0) {
1661
+ while (ctx.measureText(truncatedFontName).width >
1662
+ shrunkEffectiveMaxWidth - prefixWidth &&
1663
+ truncatedFontName.length > 0) {
1260
1664
  truncatedFontName = truncatedFontName.slice(0, -1);
1261
1665
  }
1262
1666
  ctx.fillText(truncatedFontName, currentX, currentY);
@@ -1270,7 +1674,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1270
1674
  const remainingWidth = shrunkEffectiveMaxWidth - (currentX - x);
1271
1675
  if (remainingWidth > 0) {
1272
1676
  let truncatedSuffix = suffix;
1273
- while (ctx.measureText(truncatedSuffix).width > remainingWidth && truncatedSuffix.length > 0) {
1677
+ while (ctx.measureText(truncatedSuffix).width > remainingWidth &&
1678
+ truncatedSuffix.length > 0) {
1274
1679
  truncatedSuffix = truncatedSuffix.slice(0, -1);
1275
1680
  }
1276
1681
  if (truncatedSuffix.length > 0) {
@@ -1311,7 +1716,8 @@ displayIndex, showLabels, scaleFactor, imageRefs, mockupBounds = null) => {
1311
1716
  else {
1312
1717
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1313
1718
  swatchX = swatchesStartX;
1314
- swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1719
+ swatchY =
1720
+ result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1315
1721
  currentY += result.height;
1316
1722
  }
1317
1723
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
@@ -1420,7 +1826,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1420
1826
  const ratio = img.naturalWidth / img.naturalHeight;
1421
1827
  const iconHeight = iconFontSize * 2;
1422
1828
  const iconWidth = Math.max(1, Math.floor(iconHeight * ratio));
1423
- iconImageReservedWidth = iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1829
+ iconImageReservedWidth =
1830
+ iconWidth + LAYOUT.ELEMENT_SPACING * scaleFactor;
1424
1831
  }
1425
1832
  }
1426
1833
  }
@@ -1498,7 +1905,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1498
1905
  let lineToRender = line;
1499
1906
  const lineWidth = ctx.measureText(lineToRender).width;
1500
1907
  if (lineWidth > lineEffectiveMaxWidth) {
1501
- while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth && lineToRender.length > 0) {
1908
+ while (ctx.measureText(lineToRender).width > lineEffectiveMaxWidth &&
1909
+ lineToRender.length > 0) {
1502
1910
  lineToRender = lineToRender.slice(0, -1);
1503
1911
  }
1504
1912
  }
@@ -1519,7 +1927,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1519
1927
  let lineToRender = lineText;
1520
1928
  const lineWidth = ctx.measureText(lineToRender).width;
1521
1929
  if (lineWidth > shrunkEffectiveMaxWidth) {
1522
- while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth && lineToRender.length > 0) {
1930
+ while (ctx.measureText(lineToRender).width > shrunkEffectiveMaxWidth &&
1931
+ lineToRender.length > 0) {
1523
1932
  lineToRender = lineToRender.slice(0, -1);
1524
1933
  }
1525
1934
  }
@@ -1582,7 +1991,7 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1582
1991
  const testLines = buildWrappedLines(ctx, testText, effectiveMaxWidth, x, cursorY, lineHeight, mockupBounds);
1583
1992
  testLines.length * lineHeight;
1584
1993
  // 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));
1994
+ const testTextWidth = Math.max(...testLines.map((line) => ctx.measureText(line).width));
1586
1995
  const textEndX = x + testTextWidth;
1587
1996
  const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1588
1997
  const swatchesStartX = textEndX + spacing;
@@ -1611,7 +2020,8 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1611
2020
  else {
1612
2021
  // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1613
2022
  swatchX = swatchesStartX;
1614
- swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
2023
+ swatchY =
2024
+ colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1615
2025
  // Render swatches (đã kiểm tra overflow ở trên)
1616
2026
  drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
1617
2027
  // cursorY đã được cập nhật ở trên
@@ -1627,10 +2037,7 @@ const prepareExportCanvas = async (config, options = {}) => {
1627
2037
  current: new Map(),
1628
2038
  };
1629
2039
  // Load fonts and images in parallel
1630
- await Promise.all([
1631
- preloadFonts(config),
1632
- preloadImages(config, imageRefs)
1633
- ]);
2040
+ await Promise.all([preloadFonts(config), preloadImages(config, imageRefs)]);
1634
2041
  renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
1635
2042
  if (!canvas.width || !canvas.height) {
1636
2043
  return null;