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.
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,10 +638,14 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
626
638
  renderErrorState(ctx, canvas, "Position phải là TEXT");
627
639
  return;
628
640
  }
641
+ if (!position.layer_colors || position.layer_colors.length < 3) {
642
+ renderErrorState(ctx, canvas, "Không có đủ màu cho stroke patches");
643
+ return;
644
+ }
629
645
  // 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];
646
+ const textColor = position.layer_colors[0];
647
+ const borderColor = position.layer_colors[1];
648
+ const backgroundColor = position.layer_colors[2];
633
649
  const fabricColor = position.layer_colors?.[3]; // Màu vải
634
650
  // For rendering, use fallback colors (fabricColor không cần fallback vì chỉ hiển thị)
635
651
  const textColorForRender = textColor;
@@ -921,6 +937,371 @@ const renderStrokePatchesCanvas = (ctx, canvas, config, imageRefs) => {
921
937
  }
922
938
  ctx.restore();
923
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
+ }
1134
+ infoY += infoLineHeight;
1135
+ // Reset font về mặc định cho các dòng tiếp theo
1136
+ ctx.font = `${infoFontSize}px ${LAYOUT.FONT_FAMILY}`;
1137
+ // Màu chữ (Text Color) - layer_colors[0]
1138
+ drawAsterisk(padding, infoY);
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 có 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
+ }
1165
+ }
1166
+ infoY += infoLineHeight;
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 có 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
+ }
1195
+ }
1196
+ infoY += infoLineHeight;
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 có 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
+ }
1257
+ }
1258
+ infoY += infoLineHeight;
1259
+ // Attachment
1260
+ if (position.attachment) {
1261
+ drawAsterisk(padding + bottomPadding, infoY);
1262
+ const attachmentLabel = `Attachment: ${position.attachment}`;
1263
+ ctx.fillText(attachmentLabel, startX, infoY);
1264
+ infoY += infoLineHeight;
1265
+ }
1266
+ // Size
1267
+ if (side.size) {
1268
+ drawAsterisk(padding + bottomPadding, infoY);
1269
+ const sizeLabel = `Size: ${side.size}`;
1270
+ ctx.fillText(sizeLabel, startX, infoY);
1271
+ infoY += infoLineHeight;
1272
+ }
1273
+ // Right side: Image from config.image_url
1274
+ if (config.image_url) {
1275
+ // Draw "Mockup" label
1276
+ ctx.font = `bold ${infoFontSize * 1.2}px ${LAYOUT.FONT_FAMILY}`;
1277
+ ctx.fillStyle = "#000000";
1278
+ const mockupLabel = "Mockup";
1279
+ const mockupLabelWidth = ctx.measureText(mockupLabel).width;
1280
+ const mockupLabelX = imageSectionX + (imageSectionWidth - mockupLabelWidth) / 1.2;
1281
+ ctx.fillText(mockupLabel, mockupLabelX, bottomSectionY + LAYOUT.PADDING);
1282
+ const mockupLabelHeight = infoFontSize * 1.2 + LAYOUT.LINE_GAP * 0.5;
1283
+ const img = imageRefs.current.get(config.image_url) ??
1284
+ imageRefs.current.get("mockup");
1285
+ if (img?.complete && img.naturalWidth > 0) {
1286
+ const maxImgWidth = imageSectionWidth; // Sử dụng toàn bộ width, sát lề phải
1287
+ const maxImgHeight = bottomUsableHeight - mockupLabelHeight;
1288
+ const imgAspectRatio = img.naturalWidth / img.naturalHeight;
1289
+ let drawWidth = maxImgWidth;
1290
+ let drawHeight = drawWidth / imgAspectRatio;
1291
+ if (drawHeight > maxImgHeight) {
1292
+ drawHeight = maxImgHeight;
1293
+ drawWidth = drawHeight * imgAspectRatio;
1294
+ }
1295
+ const imgX = imageSectionX + (imageSectionWidth - drawWidth) / 0.8;
1296
+ const imgY = bottomSectionY +
1297
+ LAYOUT.PADDING +
1298
+ mockupLabelHeight +
1299
+ (bottomUsableHeight - mockupLabelHeight - drawHeight) / 2;
1300
+ ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
1301
+ }
1302
+ }
1303
+ ctx.restore();
1304
+ };
924
1305
  const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
925
1306
  const ctx = canvas.getContext("2d");
926
1307
  if (!ctx)
@@ -941,6 +1322,12 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
941
1322
  renderStrokePatchesCanvas(ctx, canvas, config, imageRefs);
942
1323
  return;
943
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
+ }
944
1331
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
945
1332
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
946
1333
  if (config.image_url) {