embroidery-qc-image 1.0.32 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwhBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AA+oEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"EmbroideryQCImage.d.ts","sourceRoot":"","sources":["../../src/components/EmbroideryQCImage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAY,MAAM,UAAU,CAAC;AAChF,OAAO,yBAAyB,CAAC;AAiKjC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiiBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA2HvD,CAAC;AA8nFF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,IAAI,GAAG,IAAI,CAuBrB,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,kBAAkB,EAC1B,UAAS,8BAAmC,KAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAuBvB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
package/dist/index.esm.js CHANGED
@@ -389,6 +389,14 @@ const preloadImages = async (config, imageRefs) => {
389
389
  seen.add(characterColorUrl);
390
390
  }
391
391
  });
392
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
393
+ position.layer_colors?.forEach((color) => {
394
+ const layerColorUrl = getImageUrl("threadColor", color);
395
+ if (!seen.has(layerColorUrl)) {
396
+ entries.push({ url: layerColorUrl });
397
+ seen.add(layerColorUrl);
398
+ }
399
+ });
392
400
  }
393
401
  });
394
402
  });
@@ -582,6 +590,10 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
582
590
  position.character_colors?.forEach((color) => {
583
591
  loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
584
592
  });
593
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
594
+ position.layer_colors?.forEach((color) => {
595
+ loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
596
+ });
585
597
  }
586
598
  });
587
599
  });
@@ -624,10 +636,14 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
624
636
  renderErrorState(ctx, canvas, "Position phải là TEXT");
625
637
  return;
626
638
  }
639
+ if (!position.layer_colors || position.layer_colors.length < 3) {
640
+ renderErrorState(ctx, canvas, "Không có đủ màu cho stroke patches");
641
+ return;
642
+ }
627
643
  // Get layer colors with empty check (don't use fallback)
628
- const textColor = position.layer_colors?.[0];
629
- const borderColor = position.layer_colors?.[1];
630
- const backgroundColor = position.layer_colors?.[2];
644
+ const textColor = position.layer_colors[0];
645
+ const borderColor = position.layer_colors[1];
646
+ const backgroundColor = position.layer_colors[2];
631
647
  const fabricColor = position.layer_colors?.[3]; // Màu vải
632
648
  // For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
633
649
  const textColorForRender = textColor;
@@ -919,6 +935,371 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
919
935
  }
920
936
  ctx.restore();
921
937
  };
938
+ // Helper function để parse size từ string như "3 X 3 INCHES"
939
+ const parseSize = (sizeStr) => {
940
+ if (!sizeStr || typeof sizeStr !== "string")
941
+ return null;
942
+ // Pattern để match "3 X 3 INCHES" hoặc "3x3" hoặc "3 X 3"
943
+ const match = sizeStr.match(/(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)/i);
944
+ if (!match)
945
+ return null;
946
+ const width = parseFloat(match[1]);
947
+ const height = parseFloat(match[2]);
948
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
949
+ return null;
950
+ return { width, height };
951
+ };
952
+ const renderTemplateCustomTextPatchesCanvas = (ctx, canvas, config, imageRefs) => {
953
+ // Clear canvas
954
+ ctx.fillStyle = "#e7e7e7";
955
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
956
+ const padding = LAYOUT.PADDING * 6;
957
+ const usableWidth = canvas.width - padding * 2;
958
+ const usableHeight = canvas.height - padding * 2;
959
+ // Calculate sections
960
+ const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
961
+ const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
962
+ const topSectionY = padding;
963
+ const bottomSectionY = topSectionY + topSectionHeight;
964
+ // Get first side (template_custom_text_patches should have only one side)
965
+ const side = config.sides[0];
966
+ if (!side || !side.positions.length) {
967
+ renderErrorState(ctx, canvas, "Không có dữ liệu positions");
968
+ return;
969
+ }
970
+ const position = side.positions[0];
971
+ if (position.type !== "TEXT") {
972
+ renderErrorState(ctx, canvas, "Position phải là TEXT");
973
+ return;
974
+ }
975
+ if (!position.layer_colors || position.layer_colors.length < 3) {
976
+ renderErrorState(ctx, canvas, "Không có đủ màu cho template custom text patches");
977
+ return;
978
+ }
979
+ // Parse size từ side.size
980
+ const parsedSize = side.size ? parseSize(side.size) : null;
981
+ if (!parsedSize) {
982
+ renderErrorState(ctx, canvas, "Không thể parse size từ dữ liệu");
983
+ return;
984
+ }
985
+ // Get layer colors
986
+ const textColor = position.layer_colors[0];
987
+ const borderColor = position.layer_colors[1];
988
+ const backgroundColor = position.layer_colors[2];
989
+ const fabricColor = position.layer_colors?.[3]; // Màu vải
990
+ // Check if font is missing (but continue rendering)
991
+ const isFontMissing = !position.font || position.font.trim() === "";
992
+ // ============================================================================
993
+ // TOP SECTION (2/3): Hiển thị mẫu preview với khung hình chữ nhật
994
+ // ============================================================================
995
+ ctx.save();
996
+ // Draw "Hình mẫu:" label at the top
997
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
998
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
999
+ ctx.fillStyle = "#CC0000"; // Red color
1000
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
1001
+ // Adjust top section Y to account for title + extra spacing (40px)
1002
+ const extraSpacing = 40;
1003
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
1004
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
1005
+ // Tính kích thước khung hình chữ nhật dựa trên tỉ lệ size
1006
+ // Sử dụng tỉ lệ width/height từ parsedSize để giữ đúng aspect ratio
1007
+ const sizeAspectRatio = parsedSize.width / parsedSize.height;
1008
+ // Tính kích thước khung để fit trong top section
1009
+ let rectWidth = Math.min(usableWidth * 0.9, actualTopSectionHeight * sizeAspectRatio);
1010
+ let rectHeight = rectWidth / sizeAspectRatio;
1011
+ if (rectHeight > actualTopSectionHeight * 0.9) {
1012
+ rectHeight = actualTopSectionHeight * 0.9;
1013
+ rectWidth = rectHeight * sizeAspectRatio;
1014
+ }
1015
+ // Center the rectangle in top section
1016
+ const rectX = padding + usableWidth / 2 - rectWidth / 2;
1017
+ const rectY = actualTopSectionY + actualTopSectionHeight / 2 - rectHeight / 2;
1018
+ // Get color hex values
1019
+ const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
1020
+ const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
1021
+ const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
1022
+ // Border width gấp đôi: 4% của rectWidth, tối thiểu 40px
1023
+ const borderWidth = Math.max(40, rectWidth * 0.04);
1024
+ // Border radius để bo tròn góc
1025
+ const borderRadius = Math.min(rectWidth, rectHeight) * 0.08; // 8% của cạnh nhỏ hơn
1026
+ // Draw rectangle background với border radius
1027
+ ctx.fillStyle = bgColorHex;
1028
+ ctx.beginPath();
1029
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1030
+ ctx.fill();
1031
+ // Draw rectangle border với border radius
1032
+ ctx.strokeStyle = borderColorHex;
1033
+ ctx.lineWidth = borderWidth;
1034
+ ctx.beginPath();
1035
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1036
+ ctx.stroke();
1037
+ // Calculate text size to fit inside rectangle (với padding rất nhỏ)
1038
+ // Padding chỉ chừa một chút để tránh text chạm border
1039
+ const textPadding = Math.max(30, rectWidth * 0.03); // 3% padding, tối thiểu 30px
1040
+ const maxTextWidth = rectWidth - textPadding * 2 - borderWidth;
1041
+ const maxTextHeight = rectHeight - textPadding * 2 - borderWidth;
1042
+ const text = position.text || "";
1043
+ const textLines = text.split("\n");
1044
+ const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
1045
+ // Tìm font size tối đa để text vừa trong khung
1046
+ // Bắt đầu với kích thước lớn và giảm dần
1047
+ let previewFontSize = Math.min(rectWidth, rectHeight) * 0.8; // Start với 80% của cạnh nhỏ hơn
1048
+ let bestFontSize = 30; // Minimum font size
1049
+ // Binary search để tìm font size tối đa
1050
+ let low = 30;
1051
+ let high = previewFontSize;
1052
+ while (high - low > 1) {
1053
+ const mid = (low + high) / 2;
1054
+ ctx.font = `${mid}px ${fontToUse}`;
1055
+ // Kiểm tra width của tất cả các dòng
1056
+ const maxLineWidth = Math.max(...textLines.map(line => ctx.measureText(line).width));
1057
+ // Kiểm tra height với line height = 1.1 (giảm từ 1.2 để text to hơn)
1058
+ const lineHeight = mid * 1.1;
1059
+ const totalTextHeight = textLines.length * lineHeight;
1060
+ if (maxLineWidth <= maxTextWidth && totalTextHeight <= maxTextHeight) {
1061
+ // Font size này vừa, thử tăng lên
1062
+ bestFontSize = mid;
1063
+ low = mid;
1064
+ }
1065
+ else {
1066
+ // Font size này quá lớn, giảm xuống
1067
+ high = mid;
1068
+ }
1069
+ }
1070
+ previewFontSize = bestFontSize;
1071
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
1072
+ // Center the text inside rectangle
1073
+ const finalLineHeight = previewFontSize * 1.1;
1074
+ const totalTextHeightFinal = textLines.length * finalLineHeight;
1075
+ const textStartY = rectY + rectHeight / 2 - totalTextHeightFinal / 2;
1076
+ // Draw text inside rectangle
1077
+ ctx.fillStyle = textColorHex;
1078
+ ctx.textAlign = "center";
1079
+ ctx.textBaseline = "top";
1080
+ textLines.forEach((line, index) => {
1081
+ const lineY = textStartY + index * finalLineHeight;
1082
+ ctx.fillText(line, rectX + rectWidth / 2, lineY);
1083
+ });
1084
+ ctx.restore();
1085
+ // ============================================================================
1086
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
1087
+ // ============================================================================
1088
+ ctx.save();
1089
+ const bottomPadding = 0; // Không padding ngang, sát lề
1090
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
1091
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
1092
+ // Split bottom section: 60% left for info, 40% right for image
1093
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
1094
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
1095
+ const imageSectionX = padding + infoSectionWidth;
1096
+ // Left side: Info list
1097
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9;
1098
+ const infoLineHeight = infoFontSize * 1.4;
1099
+ let infoY = bottomSectionY + LAYOUT.PADDING;
1100
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1101
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1102
+ ctx.textAlign = "left";
1103
+ ctx.textBaseline = "top";
1104
+ // Asterisk prefix style
1105
+ const drawAsterisk = (x, y) => {
1106
+ ctx.save();
1107
+ ctx.fillStyle = "#CC0000"; // Red asterisk
1108
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1109
+ ctx.fillText("*", x, y);
1110
+ ctx.restore();
1111
+ };
1112
+ const asteriskWidth = ctx.measureText("*").width + 5;
1113
+ const startX = padding + asteriskWidth;
1114
+ // Font - render "Font: " với font mặc định, tên font với font đó
1115
+ drawAsterisk(padding, infoY);
1116
+ const fontPrefix = "Font: ";
1117
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1118
+ ctx.fillText(fontPrefix, startX, infoY);
1119
+ if (isFontMissing) {
1120
+ // Hiển thị warning màu đỏ nếu thiếu font
1121
+ ctx.fillStyle = "#CC0000"; // Red color
1122
+ ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
1123
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1124
+ }
1125
+ else {
1126
+ // Render font name với chính font đó
1127
+ const prefixWidth = ctx.measureText(fontPrefix).width;
1128
+ const fontName = position.font || LAYOUT.FONT_FAMILY;
1129
+ ctx.font = `${infoFontSize}px ${fontName}`;
1130
+ ctx.fillText(fontName, startX + prefixWidth, infoY);
1131
+ }
1132
+ infoY += infoLineHeight;
1133
+ // Reset font về mặc định cho các dòng tiếp theo
1134
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1135
+ // Màu chữ (Text Color) - layer_colors[0]
1136
+ drawAsterisk(padding, infoY);
1137
+ const textColorPrefix = "Màu chữ: ";
1138
+ ctx.fillText(textColorPrefix, startX, infoY);
1139
+ if (!textColor || textColor.trim() === "") {
1140
+ // Hiển thị warning màu đỏ nếu thiếu màu
1141
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1142
+ ctx.fillStyle = "#CC0000";
1143
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1144
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1145
+ }
1146
+ else {
1147
+ // Hiển thị tên màu
1148
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1149
+ ctx.fillText(textColor, startX + prefixWidth, infoY);
1150
+ // Draw text color swatch
1151
+ const swatchSize = infoFontSize * 1.3;
1152
+ const swatchX = startX +
1153
+ ctx.measureText(textColorPrefix + textColor).width +
1154
+ LAYOUT.ELEMENT_SPACING * 0.3;
1155
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1156
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
1157
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
1158
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
1159
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
1160
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1161
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
1162
+ }
1163
+ }
1164
+ infoY += infoLineHeight;
1165
+ // Màu nền (Background Color) - layer_colors[2]
1166
+ drawAsterisk(padding, infoY);
1167
+ const bgColorPrefix = "Màu nền: ";
1168
+ ctx.fillText(bgColorPrefix, startX, infoY);
1169
+ if (!backgroundColor || backgroundColor.trim() === "") {
1170
+ // Hiển thị warning màu đỏ nếu thiếu màu
1171
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1172
+ ctx.fillStyle = "#CC0000";
1173
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1174
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1175
+ }
1176
+ else {
1177
+ // Hiển thị tên màu
1178
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1179
+ ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
1180
+ // Draw background color swatch
1181
+ const swatchSize = infoFontSize * 1.3;
1182
+ const bgSwatchX = startX +
1183
+ ctx.measureText(bgColorPrefix + backgroundColor).width +
1184
+ LAYOUT.ELEMENT_SPACING * 0.3;
1185
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1186
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
1187
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
1188
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
1189
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
1190
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1191
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
1192
+ }
1193
+ }
1194
+ infoY += infoLineHeight;
1195
+ // Màu viền (Border Color) - layer_colors[1]
1196
+ drawAsterisk(padding, infoY);
1197
+ const borderColorPrefix = "Màu viền: ";
1198
+ ctx.fillText(borderColorPrefix, startX, infoY);
1199
+ if (!borderColor || borderColor.trim() === "") {
1200
+ // Hiển thị warning màu đỏ nếu thiếu màu
1201
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1202
+ ctx.fillStyle = "#CC0000";
1203
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1204
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1205
+ }
1206
+ else {
1207
+ // Hiển thị tên màu
1208
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1209
+ ctx.fillText(borderColor, startX + prefixWidth, infoY);
1210
+ // Draw border color swatch
1211
+ const swatchSize = infoFontSize * 1.3;
1212
+ const borderSwatchX = startX +
1213
+ ctx.measureText(borderColorPrefix + borderColor).width +
1214
+ LAYOUT.ELEMENT_SPACING * 0.3;
1215
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1216
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
1217
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
1218
+ if (borderColorSwatchImg?.complete &&
1219
+ borderColorSwatchImg.naturalHeight > 0) {
1220
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
1221
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1222
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
1223
+ }
1224
+ }
1225
+ infoY += infoLineHeight;
1226
+ // Màu vải (Fabric Color) - layer_colors[3]
1227
+ drawAsterisk(padding, infoY);
1228
+ const fabricColorPrefix = "Màu vải: ";
1229
+ ctx.fillText(fabricColorPrefix, startX, infoY);
1230
+ if (!fabricColor || fabricColor.trim() === "") {
1231
+ // Hiển thị warning màu đỏ nếu thiếu màu
1232
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1233
+ ctx.fillStyle = "#CC0000";
1234
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1235
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1236
+ }
1237
+ else {
1238
+ // Hiển thị tên màu
1239
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1240
+ ctx.fillText(fabricColor, startX + prefixWidth, infoY);
1241
+ // Draw fabric color swatch
1242
+ const swatchSize = infoFontSize * 1.3;
1243
+ const fabricSwatchX = startX +
1244
+ ctx.measureText(fabricColorPrefix + fabricColor).width +
1245
+ LAYOUT.ELEMENT_SPACING * 0.3;
1246
+ const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1247
+ const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
1248
+ const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
1249
+ if (fabricColorSwatchImg?.complete &&
1250
+ fabricColorSwatchImg.naturalHeight > 0) {
1251
+ const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
1252
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1253
+ ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
1254
+ }
1255
+ }
1256
+ infoY += infoLineHeight;
1257
+ // Attachment
1258
+ if (position.attachment) {
1259
+ drawAsterisk(padding + bottomPadding, infoY);
1260
+ const attachmentLabel = `Attachment: ${position.attachment}`;
1261
+ ctx.fillText(attachmentLabel, startX, infoY);
1262
+ infoY += infoLineHeight;
1263
+ }
1264
+ // Size
1265
+ if (side.size) {
1266
+ drawAsterisk(padding + bottomPadding, infoY);
1267
+ const sizeLabel = `Size: ${side.size}`;
1268
+ ctx.fillText(sizeLabel, startX, infoY);
1269
+ infoY += infoLineHeight;
1270
+ }
1271
+ // Right side: Image from config.image_url
1272
+ if (config.image_url) {
1273
+ // Draw "Mockup" label
1274
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
1275
+ ctx.fillStyle = "#000000";
1276
+ const mockupLabel = "Mockup";
1277
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
1278
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
1279
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
1280
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
1281
+ const img = imageRefs.current.get(config.image_url) ??
1282
+ imageRefs.current.get("mockup");
1283
+ if (img?.complete && img.naturalWidth > 0) {
1284
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
1285
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
1286
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
1287
+ let drawWidth = maxImgWidth;
1288
+ let drawHeight = drawWidth / imgAspectRatio;
1289
+ if (drawHeight > maxImgHeight) {
1290
+ drawHeight = maxImgHeight;
1291
+ drawWidth = drawHeight * imgAspectRatio;
1292
+ }
1293
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
1294
+ const imgY = bottomSectionY +
1295
+ LAYOUT.PADDING +
1296
+ mockupLabelHeight +
1297
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
1298
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
1299
+ }
1300
+ }
1301
+ ctx.restore();
1302
+ };
922
1303
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
923
1304
  const ctx = canvas.getContext("2d");
924
1305
  if (!ctx)
@@ -939,6 +1320,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
939
1320
  renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
940
1321
  return;
941
1322
  }
1323
+ // Check if this is a template_custom_text_patches layout
1324
+ const hasTemplateCustomTextPatches = config.sides.some((side) => side.item_type && side.item_type.includes("template_custom_text_patches"));
1325
+ if (hasTemplateCustomTextPatches) {
1326
+ renderTemplateCustomTextPatchesCanvas(ctx, canvas, config, imageRefs);
1327
+ return;
1328
+ }
942
1329
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
943
1330
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
944
1331
  if (config.image_url) {