embroidery-qc-image 1.0.32 → 1.0.34

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;AAgLjC,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
@@ -141,18 +141,30 @@ const loadFont = (fontName) => {
141
141
  }
142
142
  }
143
143
  return new Promise((resolve) => {
144
- const fontUrl = `${BASE_URLS.FONT}/${encodeURIComponent(fontName)}.woff2`;
145
- const fontFace = new FontFace(fontName, `url(${fontUrl})`);
146
- fontFace
147
- .load()
148
- .then((loadedFont) => {
149
- document.fonts.add(loadedFont);
150
- resolve();
151
- })
152
- .catch(() => {
153
- console.warn(`Could not load font ${fontName} from CDN`);
154
- resolve();
155
- });
144
+ const fontFormats = ['woff2', 'ttf', 'otf'];
145
+ let currentFormatIndex = 0;
146
+ const tryLoadFont = () => {
147
+ if (currentFormatIndex >= fontFormats.length) {
148
+ console.warn(`Could not load font ${fontName} from CDN (tried all formats)`);
149
+ resolve();
150
+ return;
151
+ }
152
+ const format = fontFormats[currentFormatIndex];
153
+ const fontUrl = `${BASE_URLS.FONT}/${encodeURIComponent(fontName)}.${format}`;
154
+ const fontFace = new FontFace(fontName, `url(${fontUrl})`);
155
+ fontFace
156
+ .load()
157
+ .then((loadedFont) => {
158
+ document.fonts.add(loadedFont);
159
+ resolve();
160
+ })
161
+ .catch(() => {
162
+ // Try next format
163
+ currentFormatIndex++;
164
+ tryLoadFont();
165
+ });
166
+ };
167
+ tryLoadFont();
156
168
  });
157
169
  };
158
170
  const getImageUrl = (type, value) => {
@@ -389,6 +401,14 @@ const preloadImages = async (config, imageRefs) => {
389
401
  seen.add(characterColorUrl);
390
402
  }
391
403
  });
404
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
405
+ position.layer_colors?.forEach((color) => {
406
+ const layerColorUrl = getImageUrl("threadColor", color);
407
+ if (!seen.has(layerColorUrl)) {
408
+ entries.push({ url: layerColorUrl });
409
+ seen.add(layerColorUrl);
410
+ }
411
+ });
392
412
  }
393
413
  });
394
414
  });
@@ -582,6 +602,10 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
582
602
  position.character_colors?.forEach((color) => {
583
603
  loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
584
604
  });
605
+ // Load layer_colors for TEXT (used in stroke_patches and template_custom_text_patches)
606
+ position.layer_colors?.forEach((color) => {
607
+ loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
608
+ });
585
609
  }
586
610
  });
587
611
  });
@@ -624,10 +648,14 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
624
648
  renderErrorState(ctx, canvas, "Position phải là TEXT");
625
649
  return;
626
650
  }
651
+ if (!position.layer_colors || position.layer_colors.length < 3) {
652
+ renderErrorState(ctx, canvas, "Không có đủ màu cho stroke patches");
653
+ return;
654
+ }
627
655
  // 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];
656
+ const textColor = position.layer_colors[0];
657
+ const borderColor = position.layer_colors[1];
658
+ const backgroundColor = position.layer_colors[2];
631
659
  const fabricColor = position.layer_colors?.[3]; // Màu vải
632
660
  // For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
633
661
  const textColorForRender = textColor;
@@ -919,6 +947,371 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
919
947
  }
920
948
  ctx.restore();
921
949
  };
950
+ // Helper function để parse size từ string như "3 X 3 INCHES"
951
+ const parseSize = (sizeStr) => {
952
+ if (!sizeStr || typeof sizeStr !== "string")
953
+ return null;
954
+ // Pattern để match "3 X 3 INCHES" hoặc "3x3" hoặc "3 X 3"
955
+ const match = sizeStr.match(/(\d+(?:\.\d+)?)\s*[xX]\s*(\d+(?:\.\d+)?)/i);
956
+ if (!match)
957
+ return null;
958
+ const width = parseFloat(match[1]);
959
+ const height = parseFloat(match[2]);
960
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
961
+ return null;
962
+ return { width, height };
963
+ };
964
+ const renderTemplateCustomTextPatchesCanvas = (ctx, canvas, config, imageRefs) => {
965
+ // Clear canvas
966
+ ctx.fillStyle = "#e7e7e7";
967
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
968
+ const padding = LAYOUT.PADDING * 6;
969
+ const usableWidth = canvas.width - padding * 2;
970
+ const usableHeight = canvas.height - padding * 2;
971
+ // Calculate sections
972
+ const topSectionHeight = Math.floor(usableHeight * (2 / 3)); // 2/3 top
973
+ const bottomSectionHeight = usableHeight - topSectionHeight; // 1/3 bottom
974
+ const topSectionY = padding;
975
+ const bottomSectionY = topSectionY + topSectionHeight;
976
+ // Get first side (template_custom_text_patches should have only one side)
977
+ const side = config.sides[0];
978
+ if (!side || !side.positions.length) {
979
+ renderErrorState(ctx, canvas, "Không có dữ liệu positions");
980
+ return;
981
+ }
982
+ const position = side.positions[0];
983
+ if (position.type !== "TEXT") {
984
+ renderErrorState(ctx, canvas, "Position phải là TEXT");
985
+ return;
986
+ }
987
+ if (!position.layer_colors || position.layer_colors.length < 3) {
988
+ renderErrorState(ctx, canvas, "Không có đủ màu cho template custom text patches");
989
+ return;
990
+ }
991
+ // Parse size từ side.size
992
+ const parsedSize = side.size ? parseSize(side.size) : null;
993
+ if (!parsedSize) {
994
+ renderErrorState(ctx, canvas, "Không thể parse size từ dữ liệu");
995
+ return;
996
+ }
997
+ // Get layer colors
998
+ const textColor = position.layer_colors[0];
999
+ const borderColor = position.layer_colors[1];
1000
+ const backgroundColor = position.layer_colors[2];
1001
+ const fabricColor = position.layer_colors?.[3]; // Màu vải
1002
+ // Check if font is missing (but continue rendering)
1003
+ const isFontMissing = !position.font || position.font.trim() === "";
1004
+ // ============================================================================
1005
+ // TOP SECTION (2/3): Hiển thị mẫu preview với khung hình chữ nhật
1006
+ // ============================================================================
1007
+ ctx.save();
1008
+ // Draw "Hình mẫu:" label at the top
1009
+ const titleFontSize = LAYOUT.HEADER_FONT_SIZE * 0.8;
1010
+ ctx.font = `bold ${titleFontSize}px ${LAYOUT.FONT_FAMILY}`;
1011
+ ctx.fillStyle = "#CC0000"; // Red color
1012
+ ctx.fillText("Hình mẫu:", padding, topSectionY);
1013
+ // Adjust top section Y to account for title + extra spacing (40px)
1014
+ const extraSpacing = 40;
1015
+ const actualTopSectionY = topSectionY + titleFontSize + LAYOUT.LINE_GAP + extraSpacing;
1016
+ const actualTopSectionHeight = topSectionHeight - titleFontSize - LAYOUT.LINE_GAP - extraSpacing;
1017
+ // Tính kích thước khung hình chữ nhật dựa trên tỉ lệ size
1018
+ // Sử dụng tỉ lệ width/height từ parsedSize để giữ đúng aspect ratio
1019
+ const sizeAspectRatio = parsedSize.width / parsedSize.height;
1020
+ // Tính kích thước khung để fit trong top section
1021
+ let rectWidth = Math.min(usableWidth * 0.9, actualTopSectionHeight * sizeAspectRatio);
1022
+ let rectHeight = rectWidth / sizeAspectRatio;
1023
+ if (rectHeight > actualTopSectionHeight * 0.9) {
1024
+ rectHeight = actualTopSectionHeight * 0.9;
1025
+ rectWidth = rectHeight * sizeAspectRatio;
1026
+ }
1027
+ // Center the rectangle in top section
1028
+ const rectX = padding + usableWidth / 2 - rectWidth / 2;
1029
+ const rectY = actualTopSectionY + actualTopSectionHeight / 2 - rectHeight / 2;
1030
+ // Get color hex values
1031
+ const textColorHex = COLOR_MAP[textColor] || LAYOUT.LABEL_COLOR;
1032
+ const borderColorHex = COLOR_MAP[borderColor] || LAYOUT.LABEL_COLOR;
1033
+ const bgColorHex = COLOR_MAP[backgroundColor] || "#FFFFFF";
1034
+ // Border width gấp đôi: 4% của rectWidth, tối thiểu 40px
1035
+ const borderWidth = Math.max(40, rectWidth * 0.04);
1036
+ // Border radius để bo tròn góc
1037
+ const borderRadius = Math.min(rectWidth, rectHeight) * 0.08; // 8% của cạnh nhỏ hơn
1038
+ // Draw rectangle background với border radius
1039
+ ctx.fillStyle = bgColorHex;
1040
+ ctx.beginPath();
1041
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1042
+ ctx.fill();
1043
+ // Draw rectangle border với border radius
1044
+ ctx.strokeStyle = borderColorHex;
1045
+ ctx.lineWidth = borderWidth;
1046
+ ctx.beginPath();
1047
+ ctx.roundRect(rectX, rectY, rectWidth, rectHeight, borderRadius);
1048
+ ctx.stroke();
1049
+ // Calculate text size to fit inside rectangle (với padding rất nhỏ)
1050
+ // Padding chỉ chừa một chút để tránh text chạm border
1051
+ const textPadding = Math.max(30, rectWidth * 0.03); // 3% padding, tối thiểu 30px
1052
+ const maxTextWidth = rectWidth - textPadding * 2 - borderWidth;
1053
+ const maxTextHeight = rectHeight - textPadding * 2 - borderWidth;
1054
+ const text = position.text || "";
1055
+ const textLines = text.split("\n");
1056
+ const fontToUse = isFontMissing ? LAYOUT.FONT_FAMILY : position.font;
1057
+ // Tìm font size tối đa để text vừa trong khung
1058
+ // Bắt đầu với kích thước lớn và giảm dần
1059
+ let previewFontSize = Math.min(rectWidth, rectHeight) * 0.8; // Start với 80% của cạnh nhỏ hơn
1060
+ let bestFontSize = 30; // Minimum font size
1061
+ // Binary search để tìm font size tối đa
1062
+ let low = 30;
1063
+ let high = previewFontSize;
1064
+ while (high - low > 1) {
1065
+ const mid = (low + high) / 2;
1066
+ ctx.font = `${mid}px ${fontToUse}`;
1067
+ // Kiểm tra width của tất cả các dòng
1068
+ const maxLineWidth = Math.max(...textLines.map(line => ctx.measureText(line).width));
1069
+ // Kiểm tra height với line height = 1.1 (giảm từ 1.2 để text to hơn)
1070
+ const lineHeight = mid * 1.1;
1071
+ const totalTextHeight = textLines.length * lineHeight;
1072
+ if (maxLineWidth <= maxTextWidth && totalTextHeight <= maxTextHeight) {
1073
+ // Font size này vừa, thử tăng lên
1074
+ bestFontSize = mid;
1075
+ low = mid;
1076
+ }
1077
+ else {
1078
+ // Font size này quá lớn, giảm xuống
1079
+ high = mid;
1080
+ }
1081
+ }
1082
+ previewFontSize = bestFontSize;
1083
+ ctx.font = `${previewFontSize}px ${fontToUse}`;
1084
+ // Center the text inside rectangle
1085
+ const finalLineHeight = previewFontSize * 1.1;
1086
+ const totalTextHeightFinal = textLines.length * finalLineHeight;
1087
+ const textStartY = rectY + rectHeight / 2 - totalTextHeightFinal / 2;
1088
+ // Draw text inside rectangle
1089
+ ctx.fillStyle = textColorHex;
1090
+ ctx.textAlign = "center";
1091
+ ctx.textBaseline = "top";
1092
+ textLines.forEach((line, index) => {
1093
+ const lineY = textStartY + index * finalLineHeight;
1094
+ ctx.fillText(line, rectX + rectWidth / 2, lineY);
1095
+ });
1096
+ ctx.restore();
1097
+ // ============================================================================
1098
+ // BOTTOM SECTION (1/3): Flex layout with info (left) and image (right)
1099
+ // ============================================================================
1100
+ ctx.save();
1101
+ const bottomPadding = 0; // Không padding ngang, sát lề
1102
+ const bottomUsableWidth = usableWidth; // Sử dụng toàn bộ width
1103
+ const bottomUsableHeight = bottomSectionHeight - LAYOUT.PADDING * 2; // Chỉ padding dọc
1104
+ // Split bottom section: 60% left for info, 40% right for image
1105
+ const infoSectionWidth = Math.floor(bottomUsableWidth * 0.6);
1106
+ const imageSectionWidth = bottomUsableWidth - infoSectionWidth;
1107
+ const imageSectionX = padding + infoSectionWidth;
1108
+ // Left side: Info list
1109
+ const infoFontSize = LAYOUT.OTHER_FONT_SIZE * 0.9;
1110
+ const infoLineHeight = infoFontSize * 1.4;
1111
+ let infoY = bottomSectionY + LAYOUT.PADDING;
1112
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1113
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1114
+ ctx.textAlign = "left";
1115
+ ctx.textBaseline = "top";
1116
+ // Asterisk prefix style
1117
+ const drawAsterisk = (x, y) => {
1118
+ ctx.save();
1119
+ ctx.fillStyle = "#CC0000"; // Red asterisk
1120
+ ctx.font = `bold ${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1121
+ ctx.fillText("*", x, y);
1122
+ ctx.restore();
1123
+ };
1124
+ const asteriskWidth = ctx.measureText("*").width + 5;
1125
+ const startX = padding + asteriskWidth;
1126
+ // Font - render "Font: " với font mặc định, tên font với font đó
1127
+ drawAsterisk(padding, infoY);
1128
+ const fontPrefix = "Font: ";
1129
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1130
+ ctx.fillText(fontPrefix, startX, infoY);
1131
+ if (isFontMissing) {
1132
+ // Hiển thị warning màu đỏ nếu thiếu font
1133
+ ctx.fillStyle = "#CC0000"; // Red color
1134
+ ctx.fillText("(Đang thiếu font chữ)", startX + ctx.measureText(fontPrefix).width, infoY);
1135
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1136
+ }
1137
+ else {
1138
+ // Render font name với chính font đó
1139
+ const prefixWidth = ctx.measureText(fontPrefix).width;
1140
+ const fontName = position.font || LAYOUT.FONT_FAMILY;
1141
+ ctx.font = `${infoFontSize}px ${fontName}`;
1142
+ ctx.fillText(fontName, startX + prefixWidth, infoY);
1143
+ }
1144
+ infoY += infoLineHeight;
1145
+ // Reset font về mặc định cho các dòng tiếp theo
1146
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1147
+ // Màu chữ (Text Color) - layer_colors[0]
1148
+ drawAsterisk(padding, infoY);
1149
+ const textColorPrefix = "Màu chữ: ";
1150
+ ctx.fillText(textColorPrefix, startX, infoY);
1151
+ if (!textColor || textColor.trim() === "") {
1152
+ // Hiển thị warning màu đỏ nếu thiếu màu
1153
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1154
+ ctx.fillStyle = "#CC0000";
1155
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1156
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1157
+ }
1158
+ else {
1159
+ // Hiển thị tên màu
1160
+ const prefixWidth = ctx.measureText(textColorPrefix).width;
1161
+ ctx.fillText(textColor, startX + prefixWidth, infoY);
1162
+ // Draw text color swatch
1163
+ const swatchSize = infoFontSize * 1.3;
1164
+ const swatchX = startX +
1165
+ ctx.measureText(textColorPrefix + textColor).width +
1166
+ LAYOUT.ELEMENT_SPACING * 0.3;
1167
+ const swatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1168
+ const textColorSwatchUrl = getImageUrl("threadColor", textColor);
1169
+ const textColorSwatchImg = imageRefs.current.get(textColorSwatchUrl);
1170
+ if (textColorSwatchImg?.complete && textColorSwatchImg.naturalHeight > 0) {
1171
+ const ratio = textColorSwatchImg.naturalWidth / textColorSwatchImg.naturalHeight;
1172
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1173
+ ctx.drawImage(textColorSwatchImg, swatchX, swatchY, swatchW, swatchSize);
1174
+ }
1175
+ }
1176
+ infoY += infoLineHeight;
1177
+ // Màu nền (Background Color) - layer_colors[2]
1178
+ drawAsterisk(padding, infoY);
1179
+ const bgColorPrefix = "Màu nền: ";
1180
+ ctx.fillText(bgColorPrefix, startX, infoY);
1181
+ if (!backgroundColor || backgroundColor.trim() === "") {
1182
+ // Hiển thị warning màu đỏ nếu thiếu màu
1183
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1184
+ ctx.fillStyle = "#CC0000";
1185
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1186
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1187
+ }
1188
+ else {
1189
+ // Hiển thị tên màu
1190
+ const prefixWidth = ctx.measureText(bgColorPrefix).width;
1191
+ ctx.fillText(backgroundColor, startX + prefixWidth, infoY);
1192
+ // Draw background color swatch
1193
+ const swatchSize = infoFontSize * 1.3;
1194
+ const bgSwatchX = startX +
1195
+ ctx.measureText(bgColorPrefix + backgroundColor).width +
1196
+ LAYOUT.ELEMENT_SPACING * 0.3;
1197
+ const bgSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1198
+ const bgColorSwatchUrl = getImageUrl("threadColor", backgroundColor);
1199
+ const bgColorSwatchImg = imageRefs.current.get(bgColorSwatchUrl);
1200
+ if (bgColorSwatchImg?.complete && bgColorSwatchImg.naturalHeight > 0) {
1201
+ const ratio = bgColorSwatchImg.naturalWidth / bgColorSwatchImg.naturalHeight;
1202
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1203
+ ctx.drawImage(bgColorSwatchImg, bgSwatchX, bgSwatchY, swatchW, swatchSize);
1204
+ }
1205
+ }
1206
+ infoY += infoLineHeight;
1207
+ // Màu viền (Border Color) - layer_colors[1]
1208
+ drawAsterisk(padding, infoY);
1209
+ const borderColorPrefix = "Màu viền: ";
1210
+ ctx.fillText(borderColorPrefix, startX, infoY);
1211
+ if (!borderColor || borderColor.trim() === "") {
1212
+ // Hiển thị warning màu đỏ nếu thiếu màu
1213
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1214
+ ctx.fillStyle = "#CC0000";
1215
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1216
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1217
+ }
1218
+ else {
1219
+ // Hiển thị tên màu
1220
+ const prefixWidth = ctx.measureText(borderColorPrefix).width;
1221
+ ctx.fillText(borderColor, startX + prefixWidth, infoY);
1222
+ // Draw border color swatch
1223
+ const swatchSize = infoFontSize * 1.3;
1224
+ const borderSwatchX = startX +
1225
+ ctx.measureText(borderColorPrefix + borderColor).width +
1226
+ LAYOUT.ELEMENT_SPACING * 0.3;
1227
+ const borderSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1228
+ const borderColorSwatchUrl = getImageUrl("threadColor", borderColor);
1229
+ const borderColorSwatchImg = imageRefs.current.get(borderColorSwatchUrl);
1230
+ if (borderColorSwatchImg?.complete &&
1231
+ borderColorSwatchImg.naturalHeight > 0) {
1232
+ const ratio = borderColorSwatchImg.naturalWidth / borderColorSwatchImg.naturalHeight;
1233
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1234
+ ctx.drawImage(borderColorSwatchImg, borderSwatchX, borderSwatchY, swatchW, swatchSize);
1235
+ }
1236
+ }
1237
+ infoY += infoLineHeight;
1238
+ // Màu vải (Fabric Color) - layer_colors[3]
1239
+ drawAsterisk(padding, infoY);
1240
+ const fabricColorPrefix = "Màu vải: ";
1241
+ ctx.fillText(fabricColorPrefix, startX, infoY);
1242
+ if (!fabricColor || fabricColor.trim() === "") {
1243
+ // Hiển thị warning màu đỏ nếu thiếu màu
1244
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1245
+ ctx.fillStyle = "#CC0000";
1246
+ ctx.fillText("(Chưa có màu)", startX + prefixWidth, infoY);
1247
+ ctx.fillStyle = LAYOUT.LABEL_COLOR; // Reset color
1248
+ }
1249
+ else {
1250
+ // Hiển thị tên màu
1251
+ const prefixWidth = ctx.measureText(fabricColorPrefix).width;
1252
+ ctx.fillText(fabricColor, startX + prefixWidth, infoY);
1253
+ // Draw fabric color swatch
1254
+ const swatchSize = infoFontSize * 1.3;
1255
+ const fabricSwatchX = startX +
1256
+ ctx.measureText(fabricColorPrefix + fabricColor).width +
1257
+ LAYOUT.ELEMENT_SPACING * 0.3;
1258
+ const fabricSwatchY = infoY + infoFontSize / 2 - swatchSize / 2;
1259
+ const fabricColorSwatchUrl = getImageUrl("threadColor", fabricColor);
1260
+ const fabricColorSwatchImg = imageRefs.current.get(fabricColorSwatchUrl);
1261
+ if (fabricColorSwatchImg?.complete &&
1262
+ fabricColorSwatchImg.naturalHeight > 0) {
1263
+ const ratio = fabricColorSwatchImg.naturalWidth / fabricColorSwatchImg.naturalHeight;
1264
+ const swatchW = Math.max(1, Math.floor(swatchSize * ratio));
1265
+ ctx.drawImage(fabricColorSwatchImg, fabricSwatchX, fabricSwatchY, swatchW, swatchSize);
1266
+ }
1267
+ }
1268
+ infoY += infoLineHeight;
1269
+ // Attachment
1270
+ if (position.attachment) {
1271
+ drawAsterisk(padding + bottomPadding, infoY);
1272
+ const attachmentLabel = `Attachment: ${position.attachment}`;
1273
+ ctx.fillText(attachmentLabel, startX, infoY);
1274
+ infoY += infoLineHeight;
1275
+ }
1276
+ // Size
1277
+ if (side.size) {
1278
+ drawAsterisk(padding + bottomPadding, infoY);
1279
+ const sizeLabel = `Size: ${side.size}`;
1280
+ ctx.fillText(sizeLabel, startX, infoY);
1281
+ infoY += infoLineHeight;
1282
+ }
1283
+ // Right side: Image from config.image_url
1284
+ if (config.image_url) {
1285
+ // Draw "Mockup" label
1286
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
1287
+ ctx.fillStyle = "#000000";
1288
+ const mockupLabel = "Mockup";
1289
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
1290
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
1291
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
1292
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
1293
+ const img = imageRefs.current.get(config.image_url) ??
1294
+ imageRefs.current.get("mockup");
1295
+ if (img?.complete && img.naturalWidth > 0) {
1296
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
1297
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
1298
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
1299
+ let drawWidth = maxImgWidth;
1300
+ let drawHeight = drawWidth / imgAspectRatio;
1301
+ if (drawHeight > maxImgHeight) {
1302
+ drawHeight = maxImgHeight;
1303
+ drawWidth = drawHeight * imgAspectRatio;
1304
+ }
1305
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
1306
+ const imgY = bottomSectionY +
1307
+ LAYOUT.PADDING +
1308
+ mockupLabelHeight +
1309
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
1310
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
1311
+ }
1312
+ }
1313
+ ctx.restore();
1314
+ };
922
1315
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
923
1316
  const ctx = canvas.getContext("2d");
924
1317
  if (!ctx)
@@ -939,6 +1332,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
939
1332
  renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
940
1333
  return;
941
1334
  }
1335
+ // Check if this is a template_custom_text_patches layout
1336
+ const hasTemplateCustomTextPatches = config.sides.some((side) => side.item_type && side.item_type.includes("template_custom_text_patches"));
1337
+ if (hasTemplateCustomTextPatches) {
1338
+ renderTemplateCustomTextPatchesCanvas(ctx, canvas, config, imageRefs);
1339
+ return;
1340
+ }
942
1341
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
943
1342
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
944
1343
  if (config.image_url) {