embroidery-qc-image 1.0.31 → 1.0.33

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
@@ -391,6 +391,14 @@ const preloadImages = async (config, imageRefs) => {
391
391
  seen.add(characterColorUrl);
392
392
  }
393
393
  });
394
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
395
+ position.layer_colors?.forEach((color) => {
396
+ const layerColorUrl = getImageUrl("threadColor", color);
397
+ if (!seen.has(layerColorUrl)) {
398
+ entries.push({ url: layerColorUrl });
399
+ seen.add(layerColorUrl);
400
+ }
401
+ });
394
402
  }
395
403
  });
396
404
  });
@@ -584,6 +592,10 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
584
592
  position.character_colors?.forEach((color) => {
585
593
  loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
586
594
  });
595
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
596
+ position.layer_colors?.forEach((color) => {
597
+ loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
598
+ });
587
599
  }
588
600
  });
589
601
  });
@@ -626,36 +638,21 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
626
638
  renderErrorState(ctx, canvas, "Position phải là TEXT");
627
639
  return;
628
640
  }
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")}`);
641
+ if (!position.layer_colors || position.layer_colors.length < 3) {
642
+ renderErrorState(ctx, canvas, "Không có đủ màu cho stroke patches");
657
643
  return;
658
644
  }
645
+ // Get layer colors with empty check (don't use fallback)
646
+ const textColor = position.layer_colors[0];
647
+ const borderColor = position.layer_colors[1];
648
+ const backgroundColor = position.layer_colors[2];
649
+ const fabricColor = position.layer_colors?.[3]; // Màu vải
650
+ // For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
651
+ const textColorForRender = textColor;
652
+ const borderColorForRender = borderColor;
653
+ const backgroundColorForRender = backgroundColor;
654
+ // Check if font is missing (but continue rendering)
655
+ const isFontMissing = !position.font || position.font.trim() === "";
659
656
  // ============================================================================
660
657
  // TOP SECTION (2/3): Hiển thị mẫu preview
661
658
  // ============================================================================
@@ -671,7 +668,8 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
671
668
  const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
672
669
  // Calculate text size to fit top section
673
670
  let previewFontSize = LAYOUT.HEADER_FONT_SIZE * 3; // Start with large size
674
- ctx.font = `${previewFontSize}px ${position.font}`;
671
+ const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
672
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
675
673
  const text = position.text || "";
676
674
  const maxTextWidth = usableWidth * 0.9; // Use 75% of width for better padding
677
675
  const maxTextHeight = actualTopSectionHeight; // Use 60% of height
@@ -679,23 +677,23 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
679
677
  let textWidth = ctx.measureText(text).width;
680
678
  while (textWidth > maxTextWidth && previewFontSize > 50) {
681
679
  previewFontSize *= 0.95;
682
- ctx.font = `${previewFontSize}px ${position.font}`;
680
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
683
681
  textWidth = ctx.measureText(text).width;
684
682
  }
685
683
  // Ensure text height also fits
686
684
  while (previewFontSize > maxTextHeight && previewFontSize > 50) {
687
685
  previewFontSize *= 0.95;
688
- ctx.font = `${previewFontSize}px ${position.font}`;
686
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
689
687
  }
690
688
  // Update textWidth after final scaling
691
689
  textWidth = ctx.measureText(text).width;
692
690
  // Center the text in top section
693
691
  const textX = padding + usableWidth / 2 - textWidth / 2;
694
692
  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";
693
+ // Get color hex values (use render colors with fallback)
694
+ const textColorHex = COLOR_MAP[textColorForRender] || LAYOUT.LABEL_COLOR;
695
+ const borderColorHex = COLOR_MAP[borderColorForRender] || LAYOUT.LABEL_COLOR;
696
+ const bgColorHex = COLOR_MAP[backgroundColorForRender] || "#FFFFFF";
699
697
  // Calculate stroke widths
700
698
  // Background needs to be MUCH thicker to create spacing from text
701
699
  // Border needs to be even thicker to wrap around background
@@ -755,62 +753,507 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
755
753
  const fontPrefix = "Font: ";
756
754
  ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
757
755
  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);
756
+ if (isFontMissing) {
757
+ // Hiển thị warning màu đỏ nếu thiếu font
758
+ ctx.fillStyle = "#CC0000"; // Red color
759
+ ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
760
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
761
+ }
762
+ else {
763
+ // Render font name với chính font đó
764
+ const prefixWidth = ctx.measureText(fontPrefix).width;
765
+ const fontName = position.font || LAYOUT.FONT_FAMILY;
766
+ ctx.font = `${infoFontSize}px ${fontName}`;
767
+ ctx.fillText(fontName, startX + prefixWidth, infoY);
768
+ }
769
+ infoY += infoLineHeight;
770
+ // Reset font về mặc định cho các dòng tiếp theo
771
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
772
+ // Màu chữ (Text Color) - layer_colors[0]
773
+ drawAsterisk(padding, infoY);
774
+ const textColorPrefix = "Màu chữ: ";
775
+ ctx.fillText(textColorPrefix, startX, infoY);
776
+ if (!textColor || textColor.trim() === "") {
777
+ // Hiển thị warning màu đỏ nếu thiếu màu
778
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
779
+ ctx.fillStyle = "#CC0000";
780
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
781
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
782
+ }
783
+ else {
784
+ // Hiển thị tên màu
785
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
786
+ ctx.fillText(textColor, startX + prefixWidth, infoY);
787
+ // Draw text color swatch
788
+ const swatchSize = infoFontSize * 1.3;
789
+ const swatchX = startX +
790
+ ctx.measureText(textColorPrefix + textColor).width +
791
+ LAYOUT.ELEMENT_SPACING * 0.3;
792
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
793
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
794
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
795
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
796
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
797
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
798
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
799
+ }
800
+ }
801
+ infoY += infoLineHeight;
802
+ // Màu nền (Background Color) - layer_colors[2]
803
+ drawAsterisk(padding, infoY);
804
+ const bgColorPrefix = "Màu nền: ";
805
+ ctx.fillText(bgColorPrefix, startX, infoY);
806
+ if (!backgroundColor || backgroundColor.trim() === "") {
807
+ // Hiển thị warning màu đỏ nếu thiếu màu
808
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
809
+ ctx.fillStyle = "#CC0000";
810
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
811
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
812
+ }
813
+ else {
814
+ // Hiển thị tên màu
815
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
816
+ ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
817
+ // Draw background color swatch
818
+ const swatchSize = infoFontSize * 1.3;
819
+ const bgSwatchX = startX +
820
+ ctx.measureText(bgColorPrefix + backgroundColor).width +
821
+ LAYOUT.ELEMENT_SPACING * 0.3;
822
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
823
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
824
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
825
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
826
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
827
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
828
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
829
+ }
830
+ }
831
+ infoY += infoLineHeight;
832
+ // Màu viền (Border Color) - layer_colors[1]
833
+ drawAsterisk(padding, infoY);
834
+ const borderColorPrefix = "Màu viền: ";
835
+ ctx.fillText(borderColorPrefix, startX, infoY);
836
+ if (!borderColor || borderColor.trim() === "") {
837
+ // Hiển thị warning màu đỏ nếu thiếu màu
838
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
839
+ ctx.fillStyle = "#CC0000";
840
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
841
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
842
+ }
843
+ else {
844
+ // Hiển thị tên màu
845
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
846
+ ctx.fillText(borderColor, startX + prefixWidth, infoY);
847
+ // Draw border color swatch
848
+ const swatchSize = infoFontSize * 1.3;
849
+ const borderSwatchX = startX +
850
+ ctx.measureText(borderColorPrefix + borderColor).width +
851
+ LAYOUT.ELEMENT_SPACING * 0.3;
852
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
853
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
854
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
855
+ if (borderColorSwatchImg?.complete &&
856
+ borderColorSwatchImg.naturalHeight > 0) {
857
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
858
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
859
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
860
+ }
861
+ }
862
+ infoY += infoLineHeight;
863
+ // Màu vải (Fabric Color) - layer_colors[3]
864
+ drawAsterisk(padding, infoY);
865
+ const fabricColorPrefix = "Màu vải: ";
866
+ ctx.fillText(fabricColorPrefix, startX, infoY);
867
+ if (!fabricColor || fabricColor.trim() === "") {
868
+ // Hiển thị warning màu đỏ nếu thiếu màu
869
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
870
+ ctx.fillStyle = "#CC0000";
871
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
872
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
873
+ }
874
+ else {
875
+ // Hiển thị tên màu
876
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
877
+ ctx.fillText(fabricColor, startX + prefixWidth, infoY);
878
+ // Draw fabric color swatch
879
+ const swatchSize = infoFontSize * 1.3;
880
+ const fabricSwatchX = startX +
881
+ ctx.measureText(fabricColorPrefix + fabricColor).width +
882
+ LAYOUT.ELEMENT_SPACING * 0.3;
883
+ const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
884
+ const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
885
+ const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
886
+ if (fabricColorSwatchImg?.complete &&
887
+ fabricColorSwatchImg.naturalHeight > 0) {
888
+ const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
889
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
890
+ ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
891
+ }
892
+ }
893
+ infoY += infoLineHeight;
894
+ // Attachment
895
+ if (position.attachment) {
896
+ drawAsterisk(padding + bottomPadding, infoY);
897
+ const attachmentLabel = `Attachment: ${position.attachment}`;
898
+ ctx.fillText(attachmentLabel, startX, infoY);
899
+ infoY += infoLineHeight;
900
+ }
901
+ // Size
902
+ if (side.size) {
903
+ drawAsterisk(padding + bottomPadding, infoY);
904
+ const sizeLabel = `Size: ${side.size}`;
905
+ ctx.fillText(sizeLabel, startX, infoY);
906
+ infoY += infoLineHeight;
907
+ }
908
+ // Right side: Image from config.image_url
909
+ if (config.image_url) {
910
+ // Draw "Mockup" label
911
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
912
+ ctx.fillStyle = "#000000";
913
+ const mockupLabel = "Mockup";
914
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
915
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
916
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
917
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
918
+ const img = imageRefs.current.get(config.image_url) ??
919
+ imageRefs.current.get("mockup");
920
+ if (img?.complete && img.naturalWidth > 0) {
921
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
922
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
923
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
924
+ let drawWidth = maxImgWidth;
925
+ let drawHeight = drawWidth / imgAspectRatio;
926
+ if (drawHeight > maxImgHeight) {
927
+ drawHeight = maxImgHeight;
928
+ drawWidth = drawHeight * imgAspectRatio;
929
+ }
930
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
931
+ const imgY = bottomSectionY +
932
+ LAYOUT.PADDING +
933
+ mockupLabelHeight +
934
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
935
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
936
+ }
937
+ }
938
+ ctx.restore();
939
+ };
940
+ // Helper function để parse size từ string như "3 X 3 INCHES"
941
+ const parseSize = (sizeStr) => {
942
+ if (!sizeStr || typeof sizeStr !== "string")
943
+ return null;
944
+ // Pattern để match "3 X 3 INCHES" hoặc "3x3" hoặc "3 X 3"
945
+ const match = sizeStr.match(/(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)/i);
946
+ if (!match)
947
+ return null;
948
+ const width = parseFloat(match[1]);
949
+ const height = parseFloat(match[2]);
950
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
951
+ return null;
952
+ return { width, height };
953
+ };
954
+ const renderTemplateCustomTextPatchesCanvas = (ctx, canvas, config, imageRefs) => {
955
+ // Clear canvas
956
+ ctx.fillStyle = "#e7e7e7";
957
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
958
+ const padding = LAYOUT.PADDING * 6;
959
+ const usableWidth = canvas.width - padding * 2;
960
+ const usableHeight = canvas.height - padding * 2;
961
+ // Calculate sections
962
+ const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
963
+ const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
964
+ const topSectionY = padding;
965
+ const bottomSectionY = topSectionY + topSectionHeight;
966
+ // Get first side (template_custom_text_patches should have only one side)
967
+ const side = config.sides[0];
968
+ if (!side || !side.positions.length) {
969
+ renderErrorState(ctx, canvas, "Không có dữ liệu positions");
970
+ return;
971
+ }
972
+ const position = side.positions[0];
973
+ if (position.type !== "TEXT") {
974
+ renderErrorState(ctx, canvas, "Position phải là TEXT");
975
+ return;
976
+ }
977
+ if (!position.layer_colors || position.layer_colors.length < 3) {
978
+ renderErrorState(ctx, canvas, "Không có đủ màu cho template custom text patches");
979
+ return;
980
+ }
981
+ // Parse size từ side.size
982
+ const parsedSize = side.size ? parseSize(side.size) : null;
983
+ if (!parsedSize) {
984
+ renderErrorState(ctx, canvas, "Không thể parse size từ dữ liệu");
985
+ return;
986
+ }
987
+ // Get layer colors
988
+ const textColor = position.layer_colors[0];
989
+ const borderColor = position.layer_colors[1];
990
+ const backgroundColor = position.layer_colors[2];
991
+ const fabricColor = position.layer_colors?.[3]; // Màu vải
992
+ // Check if font is missing (but continue rendering)
993
+ const isFontMissing = !position.font || position.font.trim() === "";
994
+ // ============================================================================
995
+ // TOP SECTION (2/3): Hiển thị mẫu preview với khung hình chữ nhật
996
+ // ============================================================================
997
+ ctx.save();
998
+ // Draw "Hình mẫu:" label at the top
999
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
1000
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
1001
+ ctx.fillStyle = "#CC0000"; // Red color
1002
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
1003
+ // Adjust top section Y to account for title + extra spacing (40px)
1004
+ const extraSpacing = 40;
1005
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
1006
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
1007
+ // Tính kích thước khung hình chữ nhật dựa trên tỉ lệ size
1008
+ // Sử dụng tỉ lệ width/height từ parsedSize để giữ đúng aspect ratio
1009
+ const sizeAspectRatio = parsedSize.width / parsedSize.height;
1010
+ // Tính kích thước khung để fit trong top section
1011
+ let rectWidth = Math.min(usableWidth * 0.9, actualTopSectionHeight * sizeAspectRatio);
1012
+ let rectHeight = rectWidth / sizeAspectRatio;
1013
+ if (rectHeight > actualTopSectionHeight * 0.9) {
1014
+ rectHeight = actualTopSectionHeight * 0.9;
1015
+ rectWidth = rectHeight * sizeAspectRatio;
1016
+ }
1017
+ // Center the rectangle in top section
1018
+ const rectX = padding + usableWidth / 2 - rectWidth / 2;
1019
+ const rectY = actualTopSectionY + actualTopSectionHeight / 2 - rectHeight / 2;
1020
+ // Get color hex values
1021
+ const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
1022
+ const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
1023
+ const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
1024
+ // Border width gấp đôi: 4% của rectWidth, tối thiểu 40px
1025
+ const borderWidth = Math.max(40, rectWidth * 0.04);
1026
+ // Border radius để bo tròn góc
1027
+ const borderRadius = Math.min(rectWidth, rectHeight) * 0.08; // 8% của cạnh nhỏ hơn
1028
+ // Draw rectangle background với border radius
1029
+ ctx.fillStyle = bgColorHex;
1030
+ ctx.beginPath();
1031
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1032
+ ctx.fill();
1033
+ // Draw rectangle border với border radius
1034
+ ctx.strokeStyle = borderColorHex;
1035
+ ctx.lineWidth = borderWidth;
1036
+ ctx.beginPath();
1037
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1038
+ ctx.stroke();
1039
+ // Calculate text size to fit inside rectangle (với padding rất nhỏ)
1040
+ // Padding chỉ chừa một chút để tránh text chạm border
1041
+ const textPadding = Math.max(30, rectWidth * 0.03); // 3% padding, tối thiểu 30px
1042
+ const maxTextWidth = rectWidth - textPadding * 2 - borderWidth;
1043
+ const maxTextHeight = rectHeight - textPadding * 2 - borderWidth;
1044
+ const text = position.text || "";
1045
+ const textLines = text.split("\n");
1046
+ const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
1047
+ // Tìm font size tối đa để text vừa trong khung
1048
+ // Bắt đầu với kích thước lớn và giảm dần
1049
+ let previewFontSize = Math.min(rectWidth, rectHeight) * 0.8; // Start với 80% của cạnh nhỏ hơn
1050
+ let bestFontSize = 30; // Minimum font size
1051
+ // Binary search để tìm font size tối đa
1052
+ let low = 30;
1053
+ let high = previewFontSize;
1054
+ while (high - low > 1) {
1055
+ const mid = (low + high) / 2;
1056
+ ctx.font = `${mid}px ${fontToUse}`;
1057
+ // Kiểm tra width của tất cả các dòng
1058
+ const maxLineWidth = Math.max(...textLines.map(line => ctx.measureText(line).width));
1059
+ // Kiểm tra height với line height = 1.1 (giảm từ 1.2 để text to hơn)
1060
+ const lineHeight = mid * 1.1;
1061
+ const totalTextHeight = textLines.length * lineHeight;
1062
+ if (maxLineWidth <= maxTextWidth && totalTextHeight <= maxTextHeight) {
1063
+ // Font size này vừa, thử tăng lên
1064
+ bestFontSize = mid;
1065
+ low = mid;
1066
+ }
1067
+ else {
1068
+ // Font size này quá lớn, giảm xuống
1069
+ high = mid;
1070
+ }
1071
+ }
1072
+ previewFontSize = bestFontSize;
1073
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
1074
+ // Center the text inside rectangle
1075
+ const finalLineHeight = previewFontSize * 1.1;
1076
+ const totalTextHeightFinal = textLines.length * finalLineHeight;
1077
+ const textStartY = rectY + rectHeight / 2 - totalTextHeightFinal / 2;
1078
+ // Draw text inside rectangle
1079
+ ctx.fillStyle = textColorHex;
1080
+ ctx.textAlign = "center";
1081
+ ctx.textBaseline = "top";
1082
+ textLines.forEach((line, index) => {
1083
+ const lineY = textStartY + index * finalLineHeight;
1084
+ ctx.fillText(line, rectX + rectWidth / 2, lineY);
1085
+ });
1086
+ ctx.restore();
1087
+ // ============================================================================
1088
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
1089
+ // ============================================================================
1090
+ ctx.save();
1091
+ const bottomPadding = 0; // Không padding ngang, sát lề
1092
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
1093
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
1094
+ // Split bottom section: 60% left for info, 40% right for image
1095
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
1096
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
1097
+ const imageSectionX = padding + infoSectionWidth;
1098
+ // Left side: Info list
1099
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9;
1100
+ const infoLineHeight = infoFontSize * 1.4;
1101
+ let infoY = bottomSectionY + LAYOUT.PADDING;
1102
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1103
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1104
+ ctx.textAlign = "left";
1105
+ ctx.textBaseline = "top";
1106
+ // Asterisk prefix style
1107
+ const drawAsterisk = (x, y) => {
1108
+ ctx.save();
1109
+ ctx.fillStyle = "#CC0000"; // Red asterisk
1110
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1111
+ ctx.fillText("*", x, y);
1112
+ ctx.restore();
1113
+ };
1114
+ const asteriskWidth = ctx.measureText("*").width + 5;
1115
+ const startX = padding + asteriskWidth;
1116
+ // Font - render "Font: " với font mặc định, tên font với font đó
1117
+ drawAsterisk(padding, infoY);
1118
+ const fontPrefix = "Font: ";
1119
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1120
+ ctx.fillText(fontPrefix, startX, infoY);
1121
+ if (isFontMissing) {
1122
+ // Hiển thị warning màu đỏ nếu thiếu font
1123
+ ctx.fillStyle = "#CC0000"; // Red color
1124
+ ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
1125
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1126
+ }
1127
+ else {
1128
+ // Render font name với chính font đó
1129
+ const prefixWidth = ctx.measureText(fontPrefix).width;
1130
+ const fontName = position.font || LAYOUT.FONT_FAMILY;
1131
+ ctx.font = `${infoFontSize}px ${fontName}`;
1132
+ ctx.fillText(fontName, startX + prefixWidth, infoY);
1133
+ }
762
1134
  infoY += infoLineHeight;
763
1135
  // Reset font về mặc định cho các dòng tiếp theo
764
1136
  ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
765
- // Màu chữ (Text Color)
1137
+ // Màu chữ (Text Color) - layer_colors[0]
766
1138
  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);
1139
+ const textColorPrefix = "Màu chữ: ";
1140
+ ctx.fillText(textColorPrefix, startX, infoY);
1141
+ if (!textColor || textColor.trim() === "") {
1142
+ // Hiển thị warning màu đỏ nếu thiếu màu
1143
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1144
+ ctx.fillStyle = "#CC0000";
1145
+ ctx.fillText("(Chưa màu)", startX + prefixWidth, infoY);
1146
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1147
+ }
1148
+ else {
1149
+ // Hiển thị tên màu
1150
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1151
+ ctx.fillText(textColor, startX + prefixWidth, infoY);
1152
+ // Draw text color swatch
1153
+ const swatchSize = infoFontSize * 1.3;
1154
+ const swatchX = startX +
1155
+ ctx.measureText(textColorPrefix + textColor).width +
1156
+ LAYOUT.ELEMENT_SPACING * 0.3;
1157
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1158
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
1159
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
1160
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
1161
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
1162
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1163
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
1164
+ }
781
1165
  }
782
1166
  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);
1167
+ // Màu nền (Background Color) - layer_colors[2]
1168
+ drawAsterisk(padding, infoY);
1169
+ const bgColorPrefix = "Màu nền: ";
1170
+ ctx.fillText(bgColorPrefix, startX, infoY);
1171
+ if (!backgroundColor || backgroundColor.trim() === "") {
1172
+ // Hiển thị warning màu đỏ nếu thiếu màu
1173
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1174
+ ctx.fillStyle = "#CC0000";
1175
+ ctx.fillText("(Chưa màu)", startX + prefixWidth, infoY);
1176
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1177
+ }
1178
+ else {
1179
+ // Hiển thị tên màu
1180
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1181
+ ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
1182
+ // Draw background color swatch
1183
+ const swatchSize = infoFontSize * 1.3;
1184
+ const bgSwatchX = startX +
1185
+ ctx.measureText(bgColorPrefix + backgroundColor).width +
1186
+ LAYOUT.ELEMENT_SPACING * 0.3;
1187
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1188
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
1189
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
1190
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
1191
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
1192
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1193
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
1194
+ }
796
1195
  }
797
1196
  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);
1197
+ // Màu viền (Border Color) - layer_colors[1]
1198
+ drawAsterisk(padding, infoY);
1199
+ const borderColorPrefix = "Màu viền: ";
1200
+ ctx.fillText(borderColorPrefix, startX, infoY);
1201
+ if (!borderColor || borderColor.trim() === "") {
1202
+ // Hiển thị warning màu đỏ nếu thiếu màu
1203
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1204
+ ctx.fillStyle = "#CC0000";
1205
+ ctx.fillText("(Chưa màu)", startX + prefixWidth, infoY);
1206
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1207
+ }
1208
+ else {
1209
+ // Hiển thị tên màu
1210
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1211
+ ctx.fillText(borderColor, startX + prefixWidth, infoY);
1212
+ // Draw border color swatch
1213
+ const swatchSize = infoFontSize * 1.3;
1214
+ const borderSwatchX = startX +
1215
+ ctx.measureText(borderColorPrefix + borderColor).width +
1216
+ LAYOUT.ELEMENT_SPACING * 0.3;
1217
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1218
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
1219
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
1220
+ if (borderColorSwatchImg?.complete &&
1221
+ borderColorSwatchImg.naturalHeight > 0) {
1222
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
1223
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1224
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
1225
+ }
1226
+ }
1227
+ infoY += infoLineHeight;
1228
+ // Màu vải (Fabric Color) - layer_colors[3]
1229
+ drawAsterisk(padding, infoY);
1230
+ const fabricColorPrefix = "Màu vải: ";
1231
+ ctx.fillText(fabricColorPrefix, startX, infoY);
1232
+ if (!fabricColor || fabricColor.trim() === "") {
1233
+ // Hiển thị warning màu đỏ nếu thiếu màu
1234
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1235
+ ctx.fillStyle = "#CC0000";
1236
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1237
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1238
+ }
1239
+ else {
1240
+ // Hiển thị tên màu
1241
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1242
+ ctx.fillText(fabricColor, startX + prefixWidth, infoY);
1243
+ // Draw fabric color swatch
1244
+ const swatchSize = infoFontSize * 1.3;
1245
+ const fabricSwatchX = startX +
1246
+ ctx.measureText(fabricColorPrefix + fabricColor).width +
1247
+ LAYOUT.ELEMENT_SPACING * 0.3;
1248
+ const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1249
+ const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
1250
+ const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
1251
+ if (fabricColorSwatchImg?.complete &&
1252
+ fabricColorSwatchImg.naturalHeight > 0) {
1253
+ const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
1254
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1255
+ ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
1256
+ }
814
1257
  }
815
1258
  infoY += infoLineHeight;
816
1259
  // Attachment
@@ -879,6 +1322,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
879
1322
  renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
880
1323
  return;
881
1324
  }
1325
+ // Check if this is a template_custom_text_patches layout
1326
+ const hasTemplateCustomTextPatches = config.sides.some((side) => side.item_type && side.item_type.includes("template_custom_text_patches"));
1327
+ if (hasTemplateCustomTextPatches) {
1328
+ renderTemplateCustomTextPatchesCanvas(ctx, canvas, config, imageRefs);
1329
+ return;
1330
+ }
882
1331
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
883
1332
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
884
1333
  if (config.image_url) {