embroidery-qc-image 1.0.25 → 1.0.26

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;AAibD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AAugCF,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;AAucD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmHvD,CAAC;AA2tCF,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
@@ -46,7 +46,7 @@ const LAYOUT = {
46
46
  // Font sizes (base values, will be multiplied by scaleFactor)
47
47
  HEADER_FONT_SIZE: 220,
48
48
  TEXT_FONT_SIZE: 200,
49
- OTHER_FONT_SIZE: 180,
49
+ OTHER_FONT_SIZE: 160,
50
50
  // Colors
51
51
  HEADER_COLOR: "#000000",
52
52
  LABEL_COLOR: "#444444",
@@ -60,15 +60,10 @@ const LAYOUT = {
60
60
  SECTION_SPACING: 60,
61
61
  ELEMENT_SPACING: 100,
62
62
  SWATCH_SPACING: 25,
63
- FLORAL_SPACING: 100,
64
63
  // Visual styling
65
64
  SWATCH_HEIGHT_RATIO: 2.025,
66
65
  UNDERLINE_POSITION: 0.9,
67
- UNDERLINE_WIDTH: 10,
68
- // Swatch reserved space
69
- SWATCH_RESERVED_SPACE: 1000,
70
- MIN_TEXT_WIDTH: 400,
71
- };
66
+ UNDERLINE_WIDTH: 10};
72
67
  // ============================================================================
73
68
  // HELPER FUNCTIONS
74
69
  // ============================================================================
@@ -386,6 +381,22 @@ const buildWrappedLines = (ctx, text, maxWidth) => {
386
381
  });
387
382
  return result.length ? result : [""];
388
383
  };
384
+ const calculateSwatchesWidth = (colors, swatchHeight, scaleFactor, imageRefs) => {
385
+ let totalWidth = 0;
386
+ colors.forEach((color, index) => {
387
+ const url = getImageUrl("threadColor", color);
388
+ const img = imageRefs.current.get(url);
389
+ if (img && img.complete && img.naturalHeight > 0) {
390
+ const ratio = img.naturalWidth / img.naturalHeight;
391
+ const swatchW = Math.max(1, Math.floor(swatchHeight * ratio));
392
+ totalWidth += swatchW;
393
+ if (index < colors.length - 1) {
394
+ totalWidth += LAYOUT.SWATCH_SPACING * scaleFactor;
395
+ }
396
+ }
397
+ });
398
+ return totalWidth;
399
+ };
389
400
  const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
390
401
  let swatchX = startX;
391
402
  colors.forEach((color) => {
@@ -544,22 +555,6 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
544
555
  imageRefs.current.set("mockup", mockupImage);
545
556
  }
546
557
  }
547
- const floralAssets = [];
548
- const seenFlorals = new Set();
549
- config.sides.forEach((side) => {
550
- side.positions.forEach((position) => {
551
- if (position.type === "TEXT" && position.floral_pattern) {
552
- const url = getImageUrl("floral", position.floral_pattern);
553
- if (!seenFlorals.has(url)) {
554
- const img = imageRefs.current.get(url);
555
- if (img?.complete && img.naturalWidth > 0) {
556
- floralAssets.push(img);
557
- seenFlorals.add(url);
558
- }
559
- }
560
- }
561
- });
562
- });
563
558
  const measureCanvas = document.createElement("canvas");
564
559
  measureCanvas.width = canvas.width;
565
560
  measureCanvas.height = canvas.height;
@@ -584,7 +579,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
584
579
  measureY += sideHeight + measureSpacing;
585
580
  });
586
581
  const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
587
- drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
582
+ drawMockupAndFlorals(ctx, canvas, imageRefs);
588
583
  // Render warning & message with scaleFactor and get actual heights
589
584
  let actualWarningHeight = 0;
590
585
  let actualMessageHeight = 0;
@@ -683,7 +678,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefi
683
678
  // Return the actual height of the warning (number of lines * lineHeight)
684
679
  return lines.length * lineHeight;
685
680
  };
686
- const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
681
+ const drawMockupAndFlorals = (ctx, canvas, imageRefs) => {
687
682
  const mockupImg = imageRefs.current.get("mockup");
688
683
  if (!mockupImg?.complete || !mockupImg.naturalWidth)
689
684
  return;
@@ -696,19 +691,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
696
691
  const x = canvas.width - margin - width;
697
692
  const y = canvas.height - margin - height;
698
693
  ctx.drawImage(mockupImg, x, y, width, height);
699
- // Draw florals
700
- if (floralAssets.length > 0) {
701
- const floralH = Math.min(500, height);
702
- let currentX = x - LAYOUT.FLORAL_SPACING;
703
- for (let i = floralAssets.length - 1; i >= 0; i--) {
704
- const img = floralAssets[i];
705
- const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
706
- const w = Math.max(1, Math.floor(floralH * ratio));
707
- currentX -= w;
708
- ctx.drawImage(img, currentX, y + height - floralH, w, floralH);
709
- currentX -= LAYOUT.FLORAL_SPACING;
710
- }
711
- }
694
+ // Bỏ phần vẽ florals cạnh mockup vì đã hiển thị cạnh text rồi
712
695
  };
713
696
  const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
714
697
  let currentY = startY;
@@ -730,8 +713,33 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
730
713
  ctx.stroke();
731
714
  currentY += headerResult.height + LAYOUT.SECTION_SPACING * scaleFactor;
732
715
  ctx.restore();
733
- // Compute uniform properties
716
+ // Kiểm tra xem có phải trường hợp "không thêu gì" không
734
717
  const textPositions = side.positions.filter((p) => p.type === "TEXT");
718
+ const iconPositions = side.positions.filter((p) => p.type === "ICON");
719
+ // Kiểm tra tất cả TEXT positions có trống không
720
+ // Nếu không có TEXT positions, coi như "tất cả TEXT trống" = true
721
+ const allTextEmpty = textPositions.length === 0 || textPositions.every((p) => {
722
+ const text = p.text ?? "";
723
+ return text.trim() === "";
724
+ });
725
+ // Kiểm tra tất cả ICON positions có is_delete_icon = true không
726
+ // Nếu không có ICON positions, coi như "tất cả ICON bị xóa" = true
727
+ const allIconsDeleted = iconPositions.length === 0 || iconPositions.every((p) => {
728
+ return p.is_delete_icon === true;
729
+ });
730
+ // Nếu tất cả TEXT trống và tất cả ICON bị xóa, chỉ render dòng "(không thêu gì)"
731
+ if (allTextEmpty && allIconsDeleted && side.positions.length > 0) {
732
+ ctx.save();
733
+ const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
734
+ const lineGap = LAYOUT.LINE_GAP * scaleFactor;
735
+ ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
736
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
737
+ ctx.fillText("(không thêu gì)", padding, currentY);
738
+ currentY += otherFontSize + lineGap;
739
+ ctx.restore();
740
+ return currentY - startY;
741
+ }
742
+ // Compute uniform properties
735
743
  const iconColorPositions = side.positions.filter((p) => p.type === "ICON" && (!p.layer_colors?.length || p.layer_colors.length === 1));
736
744
  const iconColorValues = iconColorPositions
737
745
  .map((p) => {
@@ -916,23 +924,87 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
916
924
  rendered++;
917
925
  }
918
926
  if (values.color && values.color !== "None" && shouldRenderField("color")) {
919
- const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
920
- const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
921
- const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
922
- const swatchX = x +
923
- Math.ceil(result.lastLineWidth) +
924
- LAYOUT.ELEMENT_SPACING * scaleFactor;
925
- const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
926
927
  const colors = values.color.includes(",")
927
928
  ? values.color.split(",").map((s) => s.trim())
928
929
  : [values.color];
930
+ const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
931
+ const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
932
+ // Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
933
+ const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, maxWidth, fontSize + lineGap);
934
+ // Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
935
+ const textEndX = x + Math.ceil(result.lastLineWidth);
936
+ const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
937
+ const swatchesStartX = textEndX + spacing;
938
+ const swatchesEndX = swatchesStartX + totalSwatchWidth;
939
+ const shouldWrapSwatches = swatchesEndX > x + maxWidth;
940
+ let swatchX;
941
+ let swatchY;
942
+ if (shouldWrapSwatches) {
943
+ // Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
944
+ swatchX = x;
945
+ swatchY = result.lastLineY + fontSize + lineGap;
946
+ cursorY += result.height + fontSize + lineGap;
947
+ }
948
+ else {
949
+ // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
950
+ swatchX = swatchesStartX;
951
+ swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
952
+ cursorY += result.height;
953
+ }
929
954
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
930
- cursorY += result.height;
955
+ if (shouldWrapSwatches) {
956
+ cursorY += swatchH;
957
+ }
931
958
  rendered++;
932
959
  }
933
960
  if (values.floral && values.floral !== "None" && shouldRenderField("floral")) {
934
- const result = wrapText(ctx, `Mẫu hoa: ${values.floral}`, x, cursorY, maxWidth, fontSize + lineGap);
935
- cursorY += result.height;
961
+ const floralUrl = getImageUrl("floral", values.floral);
962
+ const floralImg = imageRefs.current.get(floralUrl);
963
+ // Tính kích thước ảnh floral (thêm 50% = 2.5x fontSize)
964
+ const floralH = fontSize * 2.5;
965
+ let totalFloralWidth = 0;
966
+ if (floralImg?.complete && floralImg.naturalHeight > 0) {
967
+ const ratio = floralImg.naturalWidth / floralImg.naturalHeight;
968
+ totalFloralWidth = Math.max(1, Math.floor(floralH * ratio));
969
+ }
970
+ // Line height giống icon_image: floralH + lineGap
971
+ const floralLineHeight = floralH + lineGap;
972
+ // Text align bottom: đặt text ở dưới cùng của dòng
973
+ const textBottomY = cursorY + floralH;
974
+ // Đo width trước khi vẽ
975
+ ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
976
+ const labelText = `Mẫu hoa: ${values.floral}`;
977
+ const labelWidth = ctx.measureText(labelText).width;
978
+ // Vẽ text với textBaseline = bottom
979
+ ctx.textBaseline = "bottom";
980
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
981
+ ctx.fillText(labelText, x, textBottomY);
982
+ // Reset textBaseline về top cho các phần tiếp theo
983
+ ctx.textBaseline = LAYOUT.TEXT_BASELINE;
984
+ // Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
985
+ const textEndX = x + labelWidth;
986
+ const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
987
+ const floralStartX = textEndX + spacing;
988
+ const floralEndX = floralStartX + totalFloralWidth;
989
+ const shouldWrapFloral = floralEndX > x + maxWidth;
990
+ let floralX;
991
+ let floralY;
992
+ if (shouldWrapFloral) {
993
+ // Không đủ chỗ, cho ảnh floral xuống dòng mới
994
+ floralX = x;
995
+ floralY = textBottomY + lineGap;
996
+ cursorY += floralLineHeight;
997
+ }
998
+ else {
999
+ // Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
1000
+ floralX = floralStartX;
1001
+ floralY = textBottomY - floralH; // Align bottom với text
1002
+ cursorY += floralLineHeight;
1003
+ }
1004
+ // Vẽ ảnh floral
1005
+ if (floralImg?.complete && floralImg.naturalHeight > 0) {
1006
+ ctx.drawImage(floralImg, floralX, floralY, totalFloralWidth, floralH);
1007
+ }
936
1008
  rendered++;
937
1009
  }
938
1010
  if (rendered > 0)
@@ -940,40 +1012,82 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
940
1012
  ctx.restore();
941
1013
  return cursorY - y;
942
1014
  };
943
- const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLabels, scaleFactor, imageRefs) => {
1015
+ const renderTextPosition = (ctx, position, x, y, maxWidth, // tổng chiều rộng usable (không tính padding ngoài)
1016
+ displayIndex, showLabels, scaleFactor, imageRefs) => {
944
1017
  ctx.save();
945
1018
  const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
946
1019
  const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
947
1020
  const lineGap = LAYOUT.LINE_GAP * scaleFactor;
948
1021
  let currentY = y;
949
1022
  let drawnHeight = 0;
950
- // Draw label
1023
+ // Chuẩn hóa xuống dòng:
1024
+ // - Hỗ trợ cả newline thật (\n) và chuỗi literal "\\n" từ JSON
1025
+ const normalizeNewlines = (text) => text
1026
+ .replace(/\r\n/g, "\n")
1027
+ .replace(/\r/g, "\n")
1028
+ .replace(/\\n/g, "\n");
1029
+ // Get display text (handle empty/null/undefined) sau khi normalize
1030
+ const rawOriginalText = position.text ?? "";
1031
+ const normalizedText = normalizeNewlines(rawOriginalText);
1032
+ const isEmptyText = normalizedText.trim() === "";
1033
+ // ===========================================================================
1034
+ // PHẦN TEXT CHÍNH
1035
+ // - Giữ nguyên format: chỉ xuống dòng khi có '\n'
1036
+ // - Không tự wrap theo maxWidth
1037
+ // - Nếu tổng chiều ngang > maxWidth, tự động giảm font-size để vừa
1038
+ // ===========================================================================
1039
+ // Label "Text N: " luôn là font mặc định (không bị co theo nội dung)
951
1040
  const textLabel = `Text ${displayIndex}: `;
952
1041
  ctx.font = `bold ${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
953
1042
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
954
1043
  const labelWidth = ctx.measureText(textLabel).width;
955
1044
  ctx.fillText(textLabel, x, currentY);
956
- const textMaxWidth = maxWidth - labelWidth;
957
- // Get display text (handle empty/null/undefined)
958
- const isEmptyText = !position.text || position.text.trim() === "";
959
- // Draw text content - dùng font mặc định và màu đỏ
960
- ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
961
- ctx.fillStyle = DEFAULT_ERROR_COLOR;
962
- if (isEmptyText) {
963
- const textResult = wrapText(ctx, "(không có text)", x + labelWidth, currentY, textMaxWidth, textFontSize);
964
- currentY += textResult.height;
965
- drawnHeight += textResult.height;
966
- }
967
- else {
968
- const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
969
- currentY += textResult.height;
970
- drawnHeight += textResult.height;
1045
+ // Phần text value bắt đầu sau label
1046
+ const valueStartX = x + labelWidth;
1047
+ const availableWidth = Math.max(1, maxWidth - labelWidth);
1048
+ // Chuẩn hóa nội dung text để render
1049
+ const rawText = isEmptyText ? "(không có text)" : normalizedText;
1050
+ const lines = rawText.split("\n");
1051
+ // Tính font-size hiệu dụng cho phần value sao cho:
1052
+ // - Không vượt quá availableWidth
1053
+ // - Có thể thu nhỏ tùy ý (theo yêu cầu, không giới hạn tối thiểu)
1054
+ const measureMaxLineWidth = (fontSize) => {
1055
+ ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
1056
+ let maxLineWidth = 0;
1057
+ lines.forEach((line) => {
1058
+ const w = ctx.measureText(line).width;
1059
+ if (w > maxLineWidth)
1060
+ maxLineWidth = w;
1061
+ });
1062
+ return maxLineWidth;
1063
+ };
1064
+ let effectiveTextFontSize = textFontSize;
1065
+ if (!isEmptyText) {
1066
+ const baseMaxWidth = measureMaxLineWidth(textFontSize);
1067
+ if (baseMaxWidth > availableWidth) {
1068
+ const shrinkRatio = availableWidth / baseMaxWidth;
1069
+ effectiveTextFontSize = textFontSize * shrinkRatio;
1070
+ }
971
1071
  }
1072
+ // Vẽ phần value với font hiệu dụng, màu đỏ
1073
+ ctx.font = `${effectiveTextFontSize}px ${LAYOUT.FONT_FAMILY}`;
1074
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
1075
+ const valueLineHeight = effectiveTextFontSize; // giữ giống wrapText cũ (lineHeight = fontSize)
1076
+ let localY = currentY;
1077
+ lines.forEach((line, idx) => {
1078
+ ctx.fillText(line, valueStartX, localY);
1079
+ localY += valueLineHeight;
1080
+ });
1081
+ const textBlockHeight = lines.length * valueLineHeight;
1082
+ currentY += textBlockHeight;
1083
+ drawnHeight += textBlockHeight;
972
1084
  // Draw additional labels (skip when text is empty)
973
1085
  if (!isEmptyText) {
974
1086
  currentY += lineGap;
975
1087
  ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
976
1088
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
1089
+ // Lưu ý: phần dưới này vẫn có thể wrap theo maxWidth vì đây chỉ là label mô tả,
1090
+ // không phải nội dung Text chính cần giữ nguyên format.
977
1091
  if (showLabels.shape && position.text_shape) {
978
1092
  const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
979
1093
  currentY += result.height;
@@ -1004,23 +1118,90 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
1004
1118
  if (showLabels.color) {
1005
1119
  const colorValue = position.character_colors?.join(", ") || position.color;
1006
1120
  if (colorValue) {
1007
- const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
1008
- const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
1009
- const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
1010
- const swatchX = x +
1011
- Math.ceil(result.lastLineWidth) +
1012
- LAYOUT.ELEMENT_SPACING * scaleFactor;
1013
- const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1014
1121
  const colors = position.character_colors || [position.color];
1122
+ const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
1123
+ const totalSwatchWidth = calculateSwatchesWidth(colors, swatchH, scaleFactor, imageRefs);
1124
+ // Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
1125
+ const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, maxWidth, otherFontSize + lineGap);
1126
+ // Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
1127
+ const textEndX = x + Math.ceil(result.lastLineWidth);
1128
+ const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1129
+ const swatchesStartX = textEndX + spacing;
1130
+ const swatchesEndX = swatchesStartX + totalSwatchWidth;
1131
+ const shouldWrapSwatches = swatchesEndX > x + maxWidth;
1132
+ let swatchX;
1133
+ let swatchY;
1134
+ if (shouldWrapSwatches) {
1135
+ // Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
1136
+ swatchX = x;
1137
+ swatchY = result.lastLineY + otherFontSize + lineGap;
1138
+ currentY += result.height + otherFontSize + lineGap;
1139
+ drawnHeight += result.height + otherFontSize + lineGap;
1140
+ }
1141
+ else {
1142
+ // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1143
+ swatchX = swatchesStartX;
1144
+ swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1145
+ currentY += result.height;
1146
+ drawnHeight += result.height;
1147
+ }
1015
1148
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
1016
- currentY += result.height;
1017
- drawnHeight += result.height;
1149
+ if (shouldWrapSwatches) {
1150
+ currentY += swatchH;
1151
+ drawnHeight += swatchH;
1152
+ }
1018
1153
  }
1019
1154
  }
1020
1155
  if (showLabels.floral && position.floral_pattern) {
1021
- const result = wrapText(ctx, `Mẫu hoa: ${position.floral_pattern}`, x, currentY, maxWidth, otherFontSize + lineGap);
1022
- currentY += result.height;
1023
- drawnHeight += result.height;
1156
+ const floralUrl = getImageUrl("floral", position.floral_pattern);
1157
+ const floralImg = imageRefs.current.get(floralUrl);
1158
+ // Tính kích thước ảnh floral (thêm 50% = 2.5x otherFontSize)
1159
+ const floralH = otherFontSize * 2.5;
1160
+ let totalFloralWidth = 0;
1161
+ if (floralImg?.complete && floralImg.naturalHeight > 0) {
1162
+ const ratio = floralImg.naturalWidth / floralImg.naturalHeight;
1163
+ totalFloralWidth = Math.max(1, Math.floor(floralH * ratio));
1164
+ }
1165
+ // Line height giống icon_image: floralH + lineGap
1166
+ const floralLineHeight = floralH + lineGap;
1167
+ // Text align bottom: đặt text ở dưới cùng của dòng
1168
+ const textBottomY = currentY + floralH;
1169
+ // Đo width trước khi vẽ
1170
+ ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
1171
+ const labelText = `Mẫu hoa: ${position.floral_pattern}`;
1172
+ const labelWidth = ctx.measureText(labelText).width;
1173
+ // Vẽ text với textBaseline = bottom
1174
+ ctx.textBaseline = "bottom";
1175
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1176
+ ctx.fillText(labelText, x, textBottomY);
1177
+ // Reset textBaseline về top cho các phần tiếp theo
1178
+ ctx.textBaseline = LAYOUT.TEXT_BASELINE;
1179
+ // Kiểm tra xem có đủ chỗ cho ảnh floral trên cùng dòng không
1180
+ const textEndX = x + labelWidth;
1181
+ const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1182
+ const floralStartX = textEndX + spacing;
1183
+ const floralEndX = floralStartX + totalFloralWidth;
1184
+ const shouldWrapFloral = floralEndX > x + maxWidth;
1185
+ let floralX;
1186
+ let floralY;
1187
+ if (shouldWrapFloral) {
1188
+ // Không đủ chỗ, cho ảnh floral xuống dòng mới
1189
+ floralX = x;
1190
+ floralY = textBottomY + lineGap;
1191
+ currentY += floralLineHeight;
1192
+ drawnHeight += floralLineHeight;
1193
+ }
1194
+ else {
1195
+ // Đủ chỗ, vẽ ảnh floral ngay sau text trên cùng dòng
1196
+ floralX = floralStartX;
1197
+ floralY = textBottomY - floralH; // Align bottom với text
1198
+ currentY += floralLineHeight;
1199
+ drawnHeight += floralLineHeight;
1200
+ }
1201
+ // Vẽ ảnh floral
1202
+ if (floralImg?.complete && floralImg.naturalHeight > 0) {
1203
+ ctx.drawImage(floralImg, floralX, floralY, totalFloralWidth, floralH);
1204
+ }
1024
1205
  }
1025
1206
  }
1026
1207
  ctx.restore();
@@ -1055,36 +1236,46 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1055
1236
  // Fallback: hiển thị mã icon (ép sang string)
1056
1237
  iconValue = String(position.icon);
1057
1238
  }
1058
- // Vẽ label Icon: (bold, màu label mặc định)
1239
+ // Kiểm tra xem icon_image không để tính height phù hợp
1240
+ const hasIconImage = position.icon_image && position.icon_image.trim().length > 0;
1241
+ const iconImageHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
1242
+ // Text align bottom: đặt text ở dưới cùng của dòng
1243
+ const textBottomY = cursorY + iconImageHeight;
1244
+ // Đo width trước khi vẽ
1059
1245
  ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1060
- ctx.fillText(iconLabel, x, cursorY);
1061
1246
  const labelWidth = ctx.measureText(iconLabel).width;
1062
- // Vẽ value kế bên, font thường, màu đỏ giống text value
1063
1247
  ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1064
- ctx.fillStyle = DEFAULT_ERROR_COLOR;
1065
1248
  const valueText = ` ${iconValue}`;
1066
- ctx.fillText(valueText, x + labelWidth, cursorY);
1067
- // Reset lại màu về label color cho các phần text tiếp theo (màu chỉ, v.v.)
1068
- ctx.fillStyle = LAYOUT.LABEL_COLOR;
1069
1249
  const valueWidth = ctx.measureText(valueText).width;
1250
+ // Vẽ text với textBaseline = bottom
1251
+ ctx.textBaseline = "bottom";
1252
+ ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1253
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1254
+ ctx.fillText(iconLabel, x, textBottomY);
1255
+ ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1256
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
1257
+ ctx.fillText(valueText, x + labelWidth, textBottomY);
1258
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1259
+ // Reset textBaseline về top cho các phần tiếp theo
1260
+ ctx.textBaseline = LAYOUT.TEXT_BASELINE;
1070
1261
  const iconResult = {
1071
- height: iconFontSize + lineGap,
1262
+ height: iconImageHeight + lineGap,
1072
1263
  // tổng width của cả label + value, dùng để canh icon image lệch sang phải
1073
- lastLineWidth: labelWidth + valueWidth,
1074
- lastLineY: cursorY,
1075
- };
1264
+ lastLineWidth: labelWidth + valueWidth};
1076
1265
  // Draw icon image
1077
1266
  const iconUrl = getIconImageUrl(position);
1078
1267
  if (iconUrl) {
1079
1268
  const img = imageRefs.current.get(iconUrl);
1080
1269
  if (img?.complete && img.naturalHeight > 0) {
1081
1270
  const ratio = img.naturalWidth / img.naturalHeight;
1082
- const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
1271
+ // Nếu icon_image thì hiển thị to gấp đôi
1272
+ const iconHeight = hasIconImage ? iconFontSize * 2 : iconFontSize;
1273
+ const swatchW = Math.max(1, Math.floor(iconHeight * ratio));
1083
1274
  const iconX = x +
1084
1275
  Math.ceil(iconResult.lastLineWidth) +
1085
1276
  LAYOUT.ELEMENT_SPACING * scaleFactor;
1086
- const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
1087
- ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
1277
+ const iconY = textBottomY - iconHeight; // Align bottom với text
1278
+ ctx.drawImage(img, iconX, iconY, swatchW, iconHeight);
1088
1279
  }
1089
1280
  }
1090
1281
  cursorY += iconResult.height;
@@ -1101,15 +1292,39 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
1101
1292
  const hasMultiLayerColors = layerCount > 1;
1102
1293
  const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;
1103
1294
  if (iconColors?.length && !shouldSkipColorSection) {
1104
- const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
1105
- const colorResult = wrapText(ctx, `Màu chỉ: ${iconColors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
1106
- const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
1107
- const swatchX = x +
1108
- Math.ceil(colorResult.lastLineWidth) +
1109
- LAYOUT.ELEMENT_SPACING * scaleFactor;
1110
- const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
1295
+ // Dòng "Màu chỉ:" của icon dùng OTHER_FONT_SIZE, không dùng iconFontSize
1296
+ const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
1297
+ const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
1298
+ const totalSwatchWidth = calculateSwatchesWidth(iconColors, swatchH, scaleFactor, imageRefs);
1299
+ // Set font về OTHER_FONT_SIZE trước khi vẽ text "Màu chỉ:"
1300
+ ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
1301
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1302
+ // Vẽ text với maxWidth bình thường (không trừ SWATCH_RESERVED_SPACE)
1303
+ const colorResult = wrapText(ctx, `Màu chỉ: ${iconColors.join(", ")}`, x, cursorY, maxWidth, otherFontSize + lineGap);
1304
+ // Kiểm tra xem có đủ chỗ cho swatches trên cùng dòng không
1305
+ const textEndX = x + Math.ceil(colorResult.lastLineWidth);
1306
+ const spacing = LAYOUT.ELEMENT_SPACING * scaleFactor;
1307
+ const swatchesStartX = textEndX + spacing;
1308
+ const swatchesEndX = swatchesStartX + totalSwatchWidth;
1309
+ const shouldWrapSwatches = swatchesEndX > x + maxWidth;
1310
+ let swatchX;
1311
+ let swatchY;
1312
+ if (shouldWrapSwatches) {
1313
+ // Không đủ chỗ, cho TẤT CẢ swatches xuống dòng mới
1314
+ swatchX = x;
1315
+ swatchY = colorResult.lastLineY + otherFontSize + lineGap;
1316
+ cursorY += colorResult.height + otherFontSize + lineGap;
1317
+ }
1318
+ else {
1319
+ // Đủ chỗ, vẽ swatches ngay sau text trên cùng dòng
1320
+ swatchX = swatchesStartX;
1321
+ swatchY = colorResult.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
1322
+ cursorY += colorResult.height;
1323
+ }
1111
1324
  drawSwatches(ctx, iconColors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
1112
- cursorY += colorResult.height;
1325
+ if (shouldWrapSwatches) {
1326
+ cursorY += swatchH;
1327
+ }
1113
1328
  }
1114
1329
  ctx.restore();
1115
1330
  return cursorY - y;